Implement Wayland input and Linux desktop services with isolated fixtures
This commit is contained in:
parent
fa3959e221
commit
5623376284
|
|
@ -9,8 +9,10 @@ macOS builds require Xcode Command Line Tools. The Windows Win32/WGL backend
|
||||||
cross-compiles for `x86_64-windows-gnu`; runtime verification on Windows is pending. It requires
|
cross-compiles for `x86_64-windows-gnu`; runtime verification on Windows is pending. It requires
|
||||||
Windows 10 version 1703 or later and an OpenGL 3.3-capable driver.
|
Windows 10 version 1703 or later and an OpenGL 3.3-capable driver.
|
||||||
|
|
||||||
The Linux Wayland preview implements windows and EGL/OpenGL, initially tested
|
The Linux Wayland preview implements windows, EGL/OpenGL, keyboard/pointer and
|
||||||
on KDE Plasma. Input and desktop services are still pending. It vendors only ABI
|
text input, clipboard/file drops, portal dialogs/preferences, activation, icons,
|
||||||
|
and file watches, initially tested on KDE Plasma. Native menus and X11 remain
|
||||||
|
unsupported. It vendors only ABI
|
||||||
headers and protocol bindings and loads system libraries at runtime. Normal
|
headers and protocol bindings and loads system libraries at runtime. Normal
|
||||||
builds need Zig alone; the optional `shell.nix` supplies maintainer tools and
|
builds need Zig alone; the optional `shell.nix` supplies maintainer tools and
|
||||||
NixOS runtime library paths. See [Linux support and limitations](src/linux/README.md).
|
NixOS runtime library paths. See [Linux support and limitations](src/linux/README.md).
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
}:
|
}:
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
pkgs.python3
|
(pkgs.python3.withPackages (python: [ python.dbus-next ]))
|
||||||
|
pkgs.dbus
|
||||||
pkgs.wayland-scanner
|
pkgs.wayland-scanner
|
||||||
pkgs.clang-tools
|
pkgs.clang-tools
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
//! Small typed D-Bus message builder/parser. No GLib, process spawning, or
|
||||||
|
//! synchronous method calls. libdbus owns message storage and the transport.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
pub const Error = error{OutOfMemory};
|
||||||
|
pub const Iter = c.DBusMessageIter;
|
||||||
|
pub const Message = c.DBusMessage;
|
||||||
|
pub fn method(destination: [*:0]const u8, path: [*:0]const u8, interface: [*:0]const u8, member: [*:0]const u8) Error!*Message {
|
||||||
|
return c.z_dbus_message_new_method_call.?(destination, path, interface, member) orelse error.OutOfMemory;
|
||||||
|
}
|
||||||
|
pub fn append(message: *Message) Iter {
|
||||||
|
var i: Iter = undefined;
|
||||||
|
c.z_dbus_message_iter_init_append.?(message, &i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
pub fn parse(message: *Message) Iter {
|
||||||
|
var i: Iter = std.mem.zeroes(Iter);
|
||||||
|
_ = c.z_dbus_message_iter_init.?(message, &i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
pub fn basic(i: *Iter, type_: c_int, value: anytype) Error!void {
|
||||||
|
var v = value;
|
||||||
|
if (c.z_dbus_message_iter_append_basic.?(i, type_, @ptrCast(&v)) == 0) return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
pub fn string(i: *Iter, value: [*:0]const u8) Error!void {
|
||||||
|
try basic(i, c.DBUS_TYPE_STRING, value);
|
||||||
|
}
|
||||||
|
pub fn open(i: *Iter, type_: c_int, signature: ?[*:0]const u8) Error!Iter {
|
||||||
|
var nested: Iter = std.mem.zeroes(Iter);
|
||||||
|
if (c.z_dbus_message_iter_open_container.?(i, type_, signature, &nested) == 0) return error.OutOfMemory;
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
pub fn close(i: *Iter, nested: *Iter) Error!void {
|
||||||
|
if (c.z_dbus_message_iter_close_container.?(i, nested) == 0) return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
pub fn option(i: *Iter, key: [*:0]const u8, comptime signature: [:0]const u8, value: anytype) Error!void {
|
||||||
|
var pair = try open(i, c.DBUS_TYPE_DICT_ENTRY, null);
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(i, &pair);
|
||||||
|
try string(&pair, key);
|
||||||
|
var payload = try open(&pair, c.DBUS_TYPE_VARIANT, signature);
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&pair, &payload);
|
||||||
|
try basic(&payload, signature[0], value);
|
||||||
|
try close(&pair, &payload);
|
||||||
|
try close(i, &pair);
|
||||||
|
}
|
||||||
|
pub fn kind(i: *Iter) c_int {
|
||||||
|
return c.z_dbus_message_iter_get_arg_type.?(i);
|
||||||
|
}
|
||||||
|
pub fn next(i: *Iter) bool {
|
||||||
|
return c.z_dbus_message_iter_next.?(i) != 0;
|
||||||
|
}
|
||||||
|
pub fn child(i: *Iter) Iter {
|
||||||
|
var result: Iter = std.mem.zeroes(Iter);
|
||||||
|
c.z_dbus_message_iter_recurse.?(i, &result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
pub fn str(i: *Iter) ?[]const u8 {
|
||||||
|
if (kind(i) != c.DBUS_TYPE_STRING and kind(i) != c.DBUS_TYPE_OBJECT_PATH) return null;
|
||||||
|
var value: [*c]const u8 = null;
|
||||||
|
c.z_dbus_message_iter_get_basic.?(i, @ptrCast(&value));
|
||||||
|
return if (value == null) null else std.mem.span(value);
|
||||||
|
}
|
||||||
|
pub fn uint(i: *Iter) ?u32 {
|
||||||
|
if (kind(i) != c.DBUS_TYPE_UINT32) return null;
|
||||||
|
var value: u32 = 0;
|
||||||
|
c.z_dbus_message_iter_get_basic.?(i, @ptrCast(&value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
pub fn variant(i: *Iter) ?Iter {
|
||||||
|
return if (kind(i) == c.DBUS_TYPE_VARIANT) child(i) else null;
|
||||||
|
}
|
||||||
|
pub fn entry(i: *Iter, key: []const u8) ?Iter {
|
||||||
|
if (kind(i) != c.DBUS_TYPE_ARRAY) return null;
|
||||||
|
var entries = child(i);
|
||||||
|
while (kind(&entries) == c.DBUS_TYPE_DICT_ENTRY) {
|
||||||
|
var pair = child(&entries);
|
||||||
|
const name = str(&pair) orelse return null;
|
||||||
|
if (!next(&pair)) return null;
|
||||||
|
if (std.mem.eql(u8, name, key)) return pair;
|
||||||
|
if (!next(&entries)) break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
comptime {
|
||||||
|
std.debug.assert(@sizeOf(c.dbus_uint32_t) == 4);
|
||||||
|
std.debug.assert(@sizeOf(c.dbus_uint64_t) == 8);
|
||||||
|
std.debug.assert(@sizeOf(Iter) == if (@sizeOf(usize) == 8) 72 else 56);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
//! Core Wayland selection and copy-only URI drops. Transfers keep their offers
|
||||||
|
//! and sources alive until the nonblocking pipe closes, independent of focus.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const Input = @import("Input.zig");
|
||||||
|
const Files = @import("Files.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const Offer = struct {
|
||||||
|
next: ?*Offer = null,
|
||||||
|
proxy: ?*c.struct_wl_data_offer,
|
||||||
|
refs: usize = 1,
|
||||||
|
text: ?[*:0]const u8 = null,
|
||||||
|
uris: bool = false,
|
||||||
|
actions: u32 = 0,
|
||||||
|
action: u32 = 0,
|
||||||
|
};
|
||||||
|
const Source = struct { next: ?*Source = null, proxy: ?*c.struct_wl_data_source, bytes: []u8, refs: usize = 1 };
|
||||||
|
const Transfer = struct {
|
||||||
|
next: ?*Transfer = null,
|
||||||
|
fd: c_int,
|
||||||
|
request: u64 = 0,
|
||||||
|
offer: ?*Offer = null,
|
||||||
|
source: ?*Source = null,
|
||||||
|
window: ?*native.Window = null,
|
||||||
|
position: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
bytes: std.ArrayList(u8) = .empty,
|
||||||
|
offset: usize = 0,
|
||||||
|
limit: u64 = 0,
|
||||||
|
deadline: u64,
|
||||||
|
};
|
||||||
|
pub const State = struct {
|
||||||
|
manager: ?*c.struct_wl_data_device_manager = null,
|
||||||
|
device: ?*c.struct_wl_data_device = null,
|
||||||
|
offers: ?*Offer = null,
|
||||||
|
sources: ?*Source = null,
|
||||||
|
selection: ?*Offer = null,
|
||||||
|
drag: ?*Offer = null,
|
||||||
|
drag_window: ?*native.Window = null,
|
||||||
|
drag_serial: u32 = 0,
|
||||||
|
position: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
transfers: ?*Transfer = null,
|
||||||
|
};
|
||||||
|
fn b() *native.Backend {
|
||||||
|
return native.instance.?;
|
||||||
|
}
|
||||||
|
fn s() *State {
|
||||||
|
return &b().data;
|
||||||
|
}
|
||||||
|
fn data(comptime T: type, raw: ?*anyopaque) *T {
|
||||||
|
return @ptrCast(@alignCast(raw.?));
|
||||||
|
}
|
||||||
|
pub fn global(registry: ?*c.struct_wl_registry, name: u32, interface: []const u8, version: u32) void {
|
||||||
|
if (std.mem.eql(u8, interface, "wl_data_device_manager") and version >= 3)
|
||||||
|
s().manager = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_data_device_manager_interface, 3));
|
||||||
|
}
|
||||||
|
pub fn setup() void {
|
||||||
|
if (s().manager != null and Input.state().seat != null and s().device == null) {
|
||||||
|
s().device = c.wl_data_device_manager_get_data_device(s().manager, Input.state().seat);
|
||||||
|
if (s().device != null) _ = c.wl_data_device_add_listener(s().device, &device_listener, null);
|
||||||
|
}
|
||||||
|
native.setCapability(n.UHDK_CAP_CLIPBOARD, s().device != null);
|
||||||
|
native.setCapability(n.UHDK_CAP_DROP_FILES, s().device != null);
|
||||||
|
}
|
||||||
|
fn unref(o: *Offer) void {
|
||||||
|
o.refs -= 1;
|
||||||
|
if (o.refs != 0) return;
|
||||||
|
var link = &s().offers;
|
||||||
|
while (link.* != o) link = &link.*.?.next;
|
||||||
|
link.* = o.next;
|
||||||
|
c.wl_data_offer_destroy(o.proxy);
|
||||||
|
b().app.gpa.destroy(o);
|
||||||
|
}
|
||||||
|
fn offer(_: ?*anyopaque, _: ?*c.struct_wl_data_device, proxy: ?*c.struct_wl_data_offer) callconv(.c) void {
|
||||||
|
const o = b().app.gpa.create(Offer) catch {
|
||||||
|
c.wl_data_offer_destroy(proxy);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
o.* = .{ .next = s().offers, .proxy = proxy };
|
||||||
|
s().offers = o;
|
||||||
|
_ = c.wl_data_offer_add_listener(proxy, &offer_listener, o);
|
||||||
|
}
|
||||||
|
fn mime(raw: ?*anyopaque, _: ?*c.struct_wl_data_offer, name: [*c]const u8) callconv(.c) void {
|
||||||
|
const o = data(Offer, raw);
|
||||||
|
const str = std.mem.span(name);
|
||||||
|
if (std.mem.eql(u8, str, "text/plain;charset=utf-8")) o.text = "text/plain;charset=utf-8" else if (std.mem.eql(u8, str, "text/plain") and o.text == null) o.text = "text/plain";
|
||||||
|
if (std.mem.eql(u8, str, "text/uri-list")) o.uris = true;
|
||||||
|
}
|
||||||
|
fn actions(raw: ?*anyopaque, _: ?*c.struct_wl_data_offer, value: u32) callconv(.c) void {
|
||||||
|
data(Offer, raw).actions = value;
|
||||||
|
}
|
||||||
|
fn action(raw: ?*anyopaque, _: ?*c.struct_wl_data_offer, value: u32) callconv(.c) void {
|
||||||
|
data(Offer, raw).action = value;
|
||||||
|
}
|
||||||
|
const offer_listener: c.struct_wl_data_offer_listener = .{ .offer = mime, .source_actions = actions, .action = action };
|
||||||
|
fn lookup(proxy: ?*c.struct_wl_data_offer) ?*Offer {
|
||||||
|
var o = s().offers;
|
||||||
|
while (o) |item| : (o = item.next) if (item.proxy == proxy) return item;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
fn selection(_: ?*anyopaque, _: ?*c.struct_wl_data_device, proxy: ?*c.struct_wl_data_offer) callconv(.c) void {
|
||||||
|
if (s().selection) |old| unref(old);
|
||||||
|
s().selection = lookup(proxy); // Takes the initial offer reference.
|
||||||
|
}
|
||||||
|
fn enter(_: ?*anyopaque, _: ?*c.struct_wl_data_device, serial: u32, surface: ?*c.struct_wl_surface, x: i32, y: i32, proxy: ?*c.struct_wl_data_offer) callconv(.c) void {
|
||||||
|
leave(null, null);
|
||||||
|
s().drag = lookup(proxy);
|
||||||
|
s().drag_window = Input.find(surface);
|
||||||
|
s().drag_serial = serial;
|
||||||
|
s().position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
||||||
|
negotiate();
|
||||||
|
}
|
||||||
|
fn negotiate() void {
|
||||||
|
const o = s().drag orelse return;
|
||||||
|
const accepted = o.uris and s().drag_window != null and s().drag_window.?.drop_enabled;
|
||||||
|
c.wl_data_offer_accept(o.proxy, s().drag_serial, if (accepted) "text/uri-list" else null);
|
||||||
|
c.wl_data_offer_set_actions(o.proxy, if (accepted) c.WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY else 0, if (accepted) c.WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY else 0);
|
||||||
|
}
|
||||||
|
fn leave(_: ?*anyopaque, _: ?*c.struct_wl_data_device) callconv(.c) void {
|
||||||
|
if (s().drag) |o| unref(o);
|
||||||
|
s().drag = null;
|
||||||
|
s().drag_window = null;
|
||||||
|
}
|
||||||
|
fn motion(_: ?*anyopaque, _: ?*c.struct_wl_data_device, _: u32, x: i32, y: i32) callconv(.c) void {
|
||||||
|
s().position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
||||||
|
}
|
||||||
|
fn drop(_: ?*anyopaque, _: ?*c.struct_wl_data_device) callconv(.c) void {
|
||||||
|
const o = s().drag orelse return;
|
||||||
|
const w = s().drag_window orelse return;
|
||||||
|
if (!w.drop_enabled or !o.uris or o.action != c.WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY) return;
|
||||||
|
_ = receive(o, "text/uri-list", 0, 16 * 1024 * 1024, w);
|
||||||
|
// drop ends the drag session; no leave is required after a successful drop.
|
||||||
|
leave(null, null);
|
||||||
|
}
|
||||||
|
const device_listener: c.struct_wl_data_device_listener = .{ .data_offer = offer, .enter = enter, .leave = leave, .motion = motion, .drop = drop, .selection = selection };
|
||||||
|
pub fn enable(w: *native.Window, enabled: bool) u32 {
|
||||||
|
if (s().device == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
w.drop_enabled = enabled;
|
||||||
|
if (s().drag_window == w) negotiate();
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
fn sourceUnref(source: *Source) void {
|
||||||
|
source.refs -= 1;
|
||||||
|
if (source.refs != 0) return;
|
||||||
|
var link = &s().sources;
|
||||||
|
while (link.* != source) link = &link.*.?.next;
|
||||||
|
link.* = source.next;
|
||||||
|
b().app.gpa.free(source.bytes);
|
||||||
|
b().app.gpa.destroy(source);
|
||||||
|
}
|
||||||
|
fn target(_: ?*anyopaque, _: ?*c.struct_wl_data_source, _: [*c]const u8) callconv(.c) void {}
|
||||||
|
fn send(raw: ?*anyopaque, _: ?*c.struct_wl_data_source, name: [*c]const u8, fd: i32) callconv(.c) void {
|
||||||
|
const source = data(Source, raw);
|
||||||
|
const str = std.mem.span(name);
|
||||||
|
if ((!std.mem.eql(u8, str, "text/plain;charset=utf-8") and !std.mem.eql(u8, str, "text/plain")) or c.z_linux_nonblock(fd) != 0) {
|
||||||
|
_ = c.close(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transfer = b().app.gpa.create(Transfer) catch {
|
||||||
|
_ = c.close(fd);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
source.refs += 1;
|
||||||
|
transfer.* = .{ .next = s().transfers, .fd = fd, .source = source, .deadline = runtime.now(b().app.io) + 10 * std.time.ns_per_s };
|
||||||
|
s().transfers = transfer;
|
||||||
|
}
|
||||||
|
fn cancelled(raw: ?*anyopaque, proxy: ?*c.struct_wl_data_source) callconv(.c) void {
|
||||||
|
const source = data(Source, raw);
|
||||||
|
c.wl_data_source_destroy(proxy);
|
||||||
|
source.proxy = null;
|
||||||
|
sourceUnref(source);
|
||||||
|
}
|
||||||
|
fn unused(_: ?*anyopaque, _: ?*c.struct_wl_data_source) callconv(.c) void {}
|
||||||
|
fn unusedAction(_: ?*anyopaque, _: ?*c.struct_wl_data_source, _: u32) callconv(.c) void {}
|
||||||
|
const source_listener: c.struct_wl_data_source_listener = .{ .target = target, .send = send, .cancelled = cancelled, .dnd_drop_performed = unused, .dnd_finished = unused, .action = unusedAction };
|
||||||
|
pub fn write(text: n.uhdk_text) u32 {
|
||||||
|
if (s().device == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
const input = Input.state();
|
||||||
|
// Selection changes require an actual input serial. Never queue a write to
|
||||||
|
// hijack an unrelated future keystroke when the app has no input focus.
|
||||||
|
if (input.key_focus == null or input.serial == 0) return abi.UNAVAILABLE;
|
||||||
|
const source = b().app.gpa.create(Source) catch return abi.OOM;
|
||||||
|
const bytes = b().app.gpa.dupe(u8, abi.textBytes(text)) catch {
|
||||||
|
b().app.gpa.destroy(source);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
const proxy = c.wl_data_device_manager_create_data_source(s().manager) orelse {
|
||||||
|
b().app.gpa.free(bytes);
|
||||||
|
b().app.gpa.destroy(source);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
source.* = .{ .next = s().sources, .proxy = proxy, .bytes = bytes };
|
||||||
|
s().sources = source;
|
||||||
|
_ = c.wl_data_source_add_listener(proxy, &source_listener, source);
|
||||||
|
c.wl_data_source_offer(proxy, "text/plain;charset=utf-8");
|
||||||
|
c.wl_data_source_offer(proxy, "text/plain");
|
||||||
|
c.wl_data_device_set_selection(s().device, proxy, input.serial);
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
pub fn read(id: u64, limit: u64) u32 {
|
||||||
|
if (s().device == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
if (Input.state().key_focus == null) return abi.UNAVAILABLE;
|
||||||
|
const o = s().selection orelse return n.UHDK_STATUS_NOT_FOUND;
|
||||||
|
const format = o.text orelse return n.UHDK_STATUS_NOT_FOUND;
|
||||||
|
return receive(o, format, id, limit, null);
|
||||||
|
}
|
||||||
|
fn receive(o: *Offer, format: [*:0]const u8, id: u64, limit: u64, w: ?*native.Window) u32 {
|
||||||
|
var fds: [2]c_int = undefined;
|
||||||
|
if (c.z_linux_pipe(&fds) != 0) return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
const transfer = b().app.gpa.create(Transfer) catch {
|
||||||
|
_ = c.close(fds[0]);
|
||||||
|
_ = c.close(fds[1]);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
o.refs += 1;
|
||||||
|
transfer.* = .{ .next = s().transfers, .fd = fds[0], .request = id, .offer = o, .window = w, .position = s().position, .limit = limit, .deadline = runtime.now(b().app.io) + 10 * std.time.ns_per_s };
|
||||||
|
s().transfers = transfer;
|
||||||
|
c.wl_data_offer_receive(o.proxy, format, fds[1]);
|
||||||
|
_ = c.close(fds[1]);
|
||||||
|
return n.Z_DEFERRED;
|
||||||
|
}
|
||||||
|
pub fn poll(fds: *std.ArrayList(c.struct_pollfd)) !void {
|
||||||
|
var t = s().transfers;
|
||||||
|
while (t) |item| : (t = item.next) try fds.append(b().app.gpa, .{ .fd = item.fd, .events = if (item.source != null) c.POLLOUT else c.POLLIN, .revents = 0 });
|
||||||
|
}
|
||||||
|
pub fn tick() i32 {
|
||||||
|
var t = s().transfers;
|
||||||
|
while (t) |item| {
|
||||||
|
t = item.next;
|
||||||
|
if (runtime.now(b().app.io) >= item.deadline) {
|
||||||
|
finish(item, abi.UNAVAILABLE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.source) |source| {
|
||||||
|
const bytes = source.bytes[item.offset..];
|
||||||
|
if (bytes.len == 0) {
|
||||||
|
finish(item, abi.OK);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const amount = c.z_linux_pipe_write(item.fd, bytes.ptr, @min(bytes.len, 65536));
|
||||||
|
if (amount > 0) item.offset += @intCast(amount) else if (amount < 0 and c.z_linux_errno() != c.EAGAIN and c.z_linux_errno() != c.EINTR) finish(item, abi.UNAVAILABLE);
|
||||||
|
} else {
|
||||||
|
var buffer: [65536]u8 = undefined;
|
||||||
|
const amount = c.z_linux_read(item.fd, &buffer, buffer.len);
|
||||||
|
if (amount == 0) finish(item, abi.OK) else if (amount > 0) {
|
||||||
|
const length: usize = @intCast(amount);
|
||||||
|
if (length > item.limit -| item.bytes.items.len) {
|
||||||
|
finish(item, n.UHDK_STATUS_BUFFER_TOO_SMALL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
item.bytes.appendSlice(b().app.gpa, buffer[0..length]) catch {
|
||||||
|
finish(item, abi.OOM);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
} else if (c.z_linux_errno() != c.EAGAIN and c.z_linux_errno() != c.EINTR) finish(item, abi.UNAVAILABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (s().transfers != null) 100 else -1;
|
||||||
|
}
|
||||||
|
fn finish(t: *Transfer, status: u32) void {
|
||||||
|
if (t.request != 0) {
|
||||||
|
var result = std.mem.zeroes(n.z_request_result);
|
||||||
|
result.value.text = abi.textSpan(t.bytes.items);
|
||||||
|
n.z_complete(runtime.toAppHandle(b().app), t.request, if (status == abi.OK and !std.unicode.utf8ValidateSlice(t.bytes.items)) abi.INVALID else status, &result);
|
||||||
|
} else if (t.window) |w| {
|
||||||
|
if (status == abi.OK and w.drop_enabled and emitDrop(w, t.bytes.items, t.position)) c.wl_data_offer_finish(t.offer.?.proxy);
|
||||||
|
}
|
||||||
|
var link = &s().transfers;
|
||||||
|
while (link.* != t) link = &link.*.?.next;
|
||||||
|
link.* = t.next;
|
||||||
|
_ = c.close(t.fd);
|
||||||
|
if (t.offer) |o| unref(o);
|
||||||
|
if (t.source) |source| sourceUnref(source);
|
||||||
|
t.bytes.deinit(b().app.gpa);
|
||||||
|
b().app.gpa.destroy(t);
|
||||||
|
}
|
||||||
|
fn emitDrop(w: *native.Window, bytes: []const u8, position: n.uhdk_point) bool {
|
||||||
|
var files: std.ArrayList(?*n.uhdk_file) = .empty;
|
||||||
|
defer {
|
||||||
|
for (files.items) |file| n.uhdk_file_release(file);
|
||||||
|
files.deinit(b().app.gpa);
|
||||||
|
}
|
||||||
|
var lines = std.mem.splitScalar(u8, bytes, '\n');
|
||||||
|
while (lines.next()) |line| {
|
||||||
|
const uri = std.mem.trim(u8, line, "\r");
|
||||||
|
if (uri.len == 0 or uri[0] == '#') continue;
|
||||||
|
const path = Files.decode(b().app.gpa, uri) catch return false;
|
||||||
|
defer b().app.gpa.free(path);
|
||||||
|
const file = n.z_file_from_url(runtime.toAppHandle(b().app), .{ .ptr = path.ptr, .len = path.len }, null) orelse return false;
|
||||||
|
files.append(b().app.gpa, file) catch {
|
||||||
|
n.uhdk_file_release(file);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (files.items.len == 0) return false;
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_DROP_FILES;
|
||||||
|
event.data.drop_files = .{ .files = .{ .ptr = files.items.ptr, .len = files.items.len }, .position = position };
|
||||||
|
Input.send(w, &event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pub fn cancel(id: u64) bool {
|
||||||
|
var t = s().transfers;
|
||||||
|
while (t) |item| : (t = item.next) if (item.request == id) {
|
||||||
|
finish(item, abi.CANCELLED);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pub fn forget(w: *native.Window) void {
|
||||||
|
if (s().drag_window == w) {
|
||||||
|
s().drag_window = null;
|
||||||
|
negotiate();
|
||||||
|
}
|
||||||
|
var t = s().transfers;
|
||||||
|
while (t) |item| {
|
||||||
|
t = item.next;
|
||||||
|
if (item.window == w) {
|
||||||
|
item.window = null;
|
||||||
|
finish(item, abi.CANCELLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn focusLost() void {
|
||||||
|
if (s().selection) |offer_| unref(offer_);
|
||||||
|
s().selection = null;
|
||||||
|
}
|
||||||
|
pub fn releaseSeat() void {
|
||||||
|
while (s().transfers) |t| finish(t, abi.CANCELLED);
|
||||||
|
s().selection = null;
|
||||||
|
s().drag = null;
|
||||||
|
s().drag_window = null;
|
||||||
|
while (s().offers) |o| unref(o);
|
||||||
|
while (s().sources) |source| {
|
||||||
|
if (source.proxy != null) c.wl_data_source_destroy(source.proxy);
|
||||||
|
sourceUnref(source);
|
||||||
|
}
|
||||||
|
if (s().device != null) c.wl_data_device_release(s().device);
|
||||||
|
s().device = null;
|
||||||
|
native.setCapability(n.UHDK_CAP_CLIPBOARD, false);
|
||||||
|
native.setCapability(n.UHDK_CAP_DROP_FILES, false);
|
||||||
|
}
|
||||||
|
pub fn deinit() void {
|
||||||
|
releaseSeat();
|
||||||
|
if (s().manager != null) c.wl_data_device_manager_destroy(s().manager);
|
||||||
|
s().manager = null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
//! Optional Wayland activation and application icons. Activation is always a
|
||||||
|
//! compositor request, never a promise to bypass focus-stealing policy.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const Input = @import("Input.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const Activation = struct { next: ?*Activation = null, id: u64, window: *native.Window, token: ?*c.struct_xdg_activation_token_v1, deadline: u64 };
|
||||||
|
const Icon = struct { proxy: ?*c.struct_xdg_toplevel_icon_v1, buffer: ?*c.struct_wl_buffer, mapping: *anyopaque, size: usize };
|
||||||
|
pub const State = struct {
|
||||||
|
activation: ?*c.struct_xdg_activation_v1 = null,
|
||||||
|
manager: ?*c.struct_xdg_toplevel_icon_manager_v1 = null,
|
||||||
|
shm: ?*c.struct_wl_shm = null,
|
||||||
|
icon: ?Icon = null,
|
||||||
|
pending: ?*Activation = null,
|
||||||
|
launch: ?[:0]u8 = null,
|
||||||
|
};
|
||||||
|
fn b() *native.Backend {
|
||||||
|
return native.instance.?;
|
||||||
|
}
|
||||||
|
fn s() *State {
|
||||||
|
return &b().desktop;
|
||||||
|
}
|
||||||
|
pub fn global(registry: ?*c.struct_wl_registry, name: u32, interface: []const u8, _: u32) void {
|
||||||
|
if (std.mem.eql(u8, interface, "xdg_activation_v1")) s().activation = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_xdg_activation_v1_interface, 1)) else if (std.mem.eql(u8, interface, "xdg_toplevel_icon_manager_v1")) {
|
||||||
|
s().manager = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_xdg_toplevel_icon_manager_v1_interface, 1));
|
||||||
|
if (s().manager != null) _ = c.xdg_toplevel_icon_manager_v1_add_listener(s().manager, &icon_listener, null);
|
||||||
|
} else if (std.mem.eql(u8, interface, "wl_shm")) {
|
||||||
|
s().shm = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_shm_interface, 1));
|
||||||
|
if (s().shm != null) _ = c.wl_shm_add_listener(s().shm, &shm_listener, null);
|
||||||
|
}
|
||||||
|
native.setCapability(n.UHDK_CAP_APP_ICON, s().manager != null and s().shm != null);
|
||||||
|
}
|
||||||
|
fn iconSize(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel_icon_manager_v1, _: i32) callconv(.c) void {}
|
||||||
|
fn iconDone(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel_icon_manager_v1) callconv(.c) void {}
|
||||||
|
const icon_listener: c.struct_xdg_toplevel_icon_manager_v1_listener = .{ .icon_size = iconSize, .done = iconDone };
|
||||||
|
fn format(_: ?*anyopaque, _: ?*c.struct_wl_shm, _: u32) callconv(.c) void {}
|
||||||
|
const shm_listener: c.struct_wl_shm_listener = .{ .format = format };
|
||||||
|
pub fn initialize() void {
|
||||||
|
const token = c.getenv("XDG_ACTIVATION_TOKEN");
|
||||||
|
if (token != null and token[0] != 0) {
|
||||||
|
s().launch = b().app.gpa.dupeZ(u8, std.mem.span(token)) catch null;
|
||||||
|
_ = c.unsetenv("XDG_ACTIVATION_TOKEN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn focus(w: *native.Window, id: u64) u32 {
|
||||||
|
if (s().activation == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
if (s().launch) |token| {
|
||||||
|
c.xdg_activation_v1_activate(s().activation, token, w.surface);
|
||||||
|
b().app.gpa.free(token);
|
||||||
|
s().launch = null;
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
const input = Input.state();
|
||||||
|
if (input.serial == 0 or input.key_focus == null or runtime.now(b().app.io) -| input.serial_time > 5 * std.time.ns_per_s) return abi.UNAVAILABLE;
|
||||||
|
const p = b().app.gpa.create(Activation) catch return abi.OOM;
|
||||||
|
const token = c.xdg_activation_v1_get_activation_token(s().activation) orelse {
|
||||||
|
b().app.gpa.destroy(p);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
p.* = .{ .next = s().pending, .id = id, .window = w, .token = token, .deadline = runtime.now(b().app.io) + 5 * std.time.ns_per_s };
|
||||||
|
s().pending = p;
|
||||||
|
_ = c.xdg_activation_token_v1_add_listener(token, &activation_listener, p);
|
||||||
|
c.xdg_activation_token_v1_set_serial(token, input.serial, input.seat);
|
||||||
|
c.xdg_activation_token_v1_set_surface(token, input.key_focus.?.surface);
|
||||||
|
const app_id = b().app.gpa.dupeZ(u8, b().app.application_id) catch {
|
||||||
|
finish(p, abi.OOM);
|
||||||
|
return n.Z_DEFERRED;
|
||||||
|
};
|
||||||
|
defer b().app.gpa.free(app_id);
|
||||||
|
c.xdg_activation_token_v1_set_app_id(token, app_id);
|
||||||
|
c.xdg_activation_token_v1_commit(token);
|
||||||
|
return n.Z_DEFERRED;
|
||||||
|
}
|
||||||
|
fn activated(raw: ?*anyopaque, _: ?*c.struct_xdg_activation_token_v1, token: [*c]const u8) callconv(.c) void {
|
||||||
|
const p: *Activation = @ptrCast(@alignCast(raw.?));
|
||||||
|
c.xdg_activation_v1_activate(s().activation, token, p.window.surface);
|
||||||
|
finish(p, abi.OK);
|
||||||
|
}
|
||||||
|
const activation_listener: c.struct_xdg_activation_token_v1_listener = .{ .done = activated };
|
||||||
|
fn finish(p: *Activation, status: u32) void {
|
||||||
|
var link = &s().pending;
|
||||||
|
while (link.* != p) link = &link.*.?.next;
|
||||||
|
link.* = p.next;
|
||||||
|
c.xdg_activation_token_v1_destroy(p.token);
|
||||||
|
n.z_complete(runtime.toAppHandle(b().app), p.id, status, null);
|
||||||
|
b().app.gpa.destroy(p);
|
||||||
|
}
|
||||||
|
pub fn tick() i32 {
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |p| {
|
||||||
|
cursor = p.next;
|
||||||
|
if (runtime.now(b().app.io) >= p.deadline) finish(p, abi.UNAVAILABLE);
|
||||||
|
}
|
||||||
|
return if (s().pending != null) 100 else -1;
|
||||||
|
}
|
||||||
|
pub fn cancel(id: u64) bool {
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |p| : (cursor = p.next) if (p.id == id) {
|
||||||
|
finish(p, abi.CANCELLED);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pub fn forget(w: *native.Window) void {
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |p| {
|
||||||
|
cursor = p.next;
|
||||||
|
if (p.window == w) finish(p, abi.CANCELLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn applyIcon(w: *native.Window) void {
|
||||||
|
if (s().icon) |icon| c.xdg_toplevel_icon_manager_v1_set_icon(s().manager, w.top, icon.proxy);
|
||||||
|
}
|
||||||
|
/// Public pixels are straight RGBA; wl_shm ARGB8888 uses native-endian,
|
||||||
|
/// premultiplied channels. Pad rectangular images to a transparent square.
|
||||||
|
pub fn pixels(destination: []u32, side: usize, image: n.uhdk_image) void {
|
||||||
|
@memset(destination, 0);
|
||||||
|
const source = abi.bytes(image.pixels);
|
||||||
|
const x0 = (side - image.width) / 2;
|
||||||
|
const y0 = (side - image.height) / 2;
|
||||||
|
for (0..image.height) |y| for (0..image.width) |x| {
|
||||||
|
const offset = y * @as(usize, @intCast(image.stride_bytes)) + x * 4;
|
||||||
|
const a: u32 = source[offset + 3];
|
||||||
|
const r = (@as(u32, source[offset]) * a + 127) / 255;
|
||||||
|
const g = (@as(u32, source[offset + 1]) * a + 127) / 255;
|
||||||
|
const blue = (@as(u32, source[offset + 2]) * a + 127) / 255;
|
||||||
|
destination[(y + y0) * side + x + x0] = a << 24 | r << 16 | g << 8 | blue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn setIcon(image: n.uhdk_image) u32 {
|
||||||
|
if (s().manager == null or s().shm == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
const side = @max(image.width, image.height);
|
||||||
|
const size: usize = @as(usize, side) * side * 4;
|
||||||
|
const fd = c.memfd_create("uhdk-icon", c.MFD_CLOEXEC);
|
||||||
|
if (fd < 0) return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
defer _ = c.close(fd);
|
||||||
|
if (c.ftruncate(fd, @intCast(size)) != 0) return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
const mapping = c.mmap(null, size, c.PROT_READ | c.PROT_WRITE, c.MAP_SHARED, fd, 0);
|
||||||
|
if (mapping == c.MAP_FAILED) return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
var retained = false;
|
||||||
|
defer if (!retained) {
|
||||||
|
_ = c.munmap(mapping, size);
|
||||||
|
};
|
||||||
|
const words: [*]u32 = @ptrCast(@alignCast(mapping.?));
|
||||||
|
pixels(words[0 .. size / 4], side, image);
|
||||||
|
const pool = c.wl_shm_create_pool(s().shm, fd, @intCast(size)) orelse return abi.OOM;
|
||||||
|
defer c.wl_shm_pool_destroy(pool);
|
||||||
|
const buffer = c.wl_shm_pool_create_buffer(pool, 0, @intCast(side), @intCast(side), @intCast(side * 4), c.WL_SHM_FORMAT_ARGB8888) orelse return abi.OOM;
|
||||||
|
const proxy = c.xdg_toplevel_icon_manager_v1_create_icon(s().manager) orelse {
|
||||||
|
c.wl_buffer_destroy(buffer);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
c.xdg_toplevel_icon_v1_add_buffer(proxy, buffer, 1);
|
||||||
|
const old = s().icon;
|
||||||
|
s().icon = .{ .proxy = proxy, .buffer = buffer, .mapping = mapping.?, .size = size };
|
||||||
|
retained = true;
|
||||||
|
var cursor = b().windows;
|
||||||
|
while (cursor) |w| : (cursor = w.next) applyIcon(w);
|
||||||
|
if (old) |icon| destroyIcon(icon);
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
fn destroyIcon(icon: Icon) void {
|
||||||
|
c.xdg_toplevel_icon_v1_destroy(icon.proxy);
|
||||||
|
c.wl_buffer_destroy(icon.buffer);
|
||||||
|
_ = c.munmap(icon.mapping, icon.size);
|
||||||
|
}
|
||||||
|
pub fn deinit() void {
|
||||||
|
while (s().pending) |p| finish(p, abi.CANCELLED);
|
||||||
|
if (s().icon) |icon| destroyIcon(icon);
|
||||||
|
if (s().launch) |token| b().app.gpa.free(token);
|
||||||
|
if (s().manager != null) c.xdg_toplevel_icon_manager_v1_destroy(s().manager);
|
||||||
|
if (s().shm != null) c.wl_shm_destroy(s().shm);
|
||||||
|
if (s().activation != null) c.xdg_activation_v1_destroy(s().activation);
|
||||||
|
}
|
||||||
|
test "icons premultiply RGBA and pad rectangular images" {
|
||||||
|
const rgba = [_]u8{ 255, 128, 0, 128, 0, 255, 0, 255 };
|
||||||
|
var image = std.mem.zeroes(n.uhdk_image);
|
||||||
|
image.width = 2;
|
||||||
|
image.height = 1;
|
||||||
|
image.stride_bytes = 8;
|
||||||
|
image.pixels = .{ .ptr = &rgba, .len = rgba.len };
|
||||||
|
var result: [4]u32 = undefined;
|
||||||
|
pixels(&result, 2, image);
|
||||||
|
try std.testing.expectEqualSlices(u32, &.{ 0x80804000, 0xff00ff00, 0, 0 }, &result);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ const n = @import("native");
|
||||||
const abi = @import("../abi.zig");
|
const abi = @import("../abi.zig");
|
||||||
const runtime = @import("../runtime.zig");
|
const runtime = @import("../runtime.zig");
|
||||||
|
|
||||||
fn decode(allocator: std.mem.Allocator, uri: []const u8) ![]u8 {
|
pub fn decode(allocator: std.mem.Allocator, uri: []const u8) ![]u8 {
|
||||||
const encoded = if (std.mem.startsWith(u8, uri, "file:///")) uri[7..] else if (std.mem.startsWith(u8, uri, "file://localhost/")) uri[16..] else return error.Invalid;
|
const encoded = if (std.mem.startsWith(u8, uri, "file:///")) uri[7..] else if (std.mem.startsWith(u8, uri, "file://localhost/")) uri[16..] else return error.Invalid;
|
||||||
var result: std.Io.Writer.Allocating = .init(allocator);
|
var result: std.Io.Writer.Allocating = .init(allocator);
|
||||||
errdefer result.deinit();
|
errdefer result.deinit();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
//! One active Wayland seat. All mutable input state belongs to the native thread.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const Keyboard = @import("Keyboard.zig");
|
||||||
|
const Data = @import("Data.zig");
|
||||||
|
const Text = @import("Text.zig");
|
||||||
|
|
||||||
|
pub const State = struct {
|
||||||
|
seat: ?*c.struct_wl_seat = null,
|
||||||
|
seat_name: u32 = 0,
|
||||||
|
keyboard: ?*c.struct_wl_keyboard = null,
|
||||||
|
pointer: ?*c.struct_wl_pointer = null,
|
||||||
|
cursor_manager: ?*c.struct_wp_cursor_shape_manager_v1 = null,
|
||||||
|
cursor_device: ?*c.struct_wp_cursor_shape_device_v1 = null,
|
||||||
|
text_manager: ?*c.struct_zwp_text_input_manager_v3 = null,
|
||||||
|
text_input: ?*c.struct_zwp_text_input_v3 = null,
|
||||||
|
text_focus: ?*native.Window = null,
|
||||||
|
text_serial: u32 = 0,
|
||||||
|
key_focus: ?*native.Window = null,
|
||||||
|
pointer_focus: ?*native.Window = null,
|
||||||
|
context: ?*c.struct_xkb_context = null,
|
||||||
|
keymap: ?*c.struct_xkb_keymap = null,
|
||||||
|
state: ?*c.struct_xkb_state = null,
|
||||||
|
compose_table: ?*c.struct_xkb_compose_table = null,
|
||||||
|
compose: ?*c.struct_xkb_compose_state = null,
|
||||||
|
modifiers: u32 = 0,
|
||||||
|
active: bool = false,
|
||||||
|
serial: u32 = 0,
|
||||||
|
serial_time: u64 = 0,
|
||||||
|
pointer_serial: u32 = 0,
|
||||||
|
next_id: u64 = 0,
|
||||||
|
position: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
buttons: u32 = 0,
|
||||||
|
captured: bool = false,
|
||||||
|
last_click_time: u64 = 0,
|
||||||
|
last_click_button: u32 = 0,
|
||||||
|
last_click_position: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
click_count: u32 = 0,
|
||||||
|
repeat_key: ?u32 = null,
|
||||||
|
repeat_rate: u32 = 0,
|
||||||
|
repeat_delay: u32 = 600,
|
||||||
|
repeat_at: u64 = 0,
|
||||||
|
held: [256]bool = @splat(false),
|
||||||
|
axis: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
wheel: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
axis_source: u32 = c.WL_POINTER_AXIS_SOURCE_WHEEL,
|
||||||
|
axis_stop: bool = false,
|
||||||
|
scrolling: bool = false,
|
||||||
|
};
|
||||||
|
fn b() *native.Backend {
|
||||||
|
return native.instance.?;
|
||||||
|
}
|
||||||
|
pub fn state() *State {
|
||||||
|
return &b().input;
|
||||||
|
}
|
||||||
|
pub fn find(surface: ?*c.struct_wl_surface) ?*native.Window {
|
||||||
|
var item = b().windows;
|
||||||
|
while (item) |w| : (item = w.next) if (w.surface == surface) return w;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
pub fn send(w: *native.Window, event: *n.z_event) void {
|
||||||
|
event.window = w.handle;
|
||||||
|
n.z_emit(runtime.toAppHandle(w.owner), event);
|
||||||
|
}
|
||||||
|
pub fn id() u64 {
|
||||||
|
state().next_id += 1;
|
||||||
|
return state().next_id;
|
||||||
|
}
|
||||||
|
pub fn provenance(serial: u32) void {
|
||||||
|
state().serial = serial;
|
||||||
|
state().serial_time = runtime.now(b().app.io);
|
||||||
|
}
|
||||||
|
pub fn initialize() void {
|
||||||
|
const s = state();
|
||||||
|
if (c.z_linux_xkb_available() != 0) {
|
||||||
|
s.context = c.z_xkb_context_new.?(c.XKB_CONTEXT_NO_FLAGS);
|
||||||
|
if (s.context != null) {
|
||||||
|
var locale: [*c]const u8 = c.getenv("LC_ALL");
|
||||||
|
if (locale == null or locale[0] == 0) locale = c.getenv("LC_CTYPE");
|
||||||
|
if (locale == null or locale[0] == 0) locale = c.getenv("LANG");
|
||||||
|
if (locale == null or locale[0] == 0) locale = "C.UTF-8";
|
||||||
|
s.compose_table = c.z_xkb_compose_table_new_from_locale.?(s.context, locale, c.XKB_COMPOSE_COMPILE_NO_FLAGS);
|
||||||
|
if (s.compose_table != null) s.compose = c.z_xkb_compose_state_new.?(s.compose_table, c.XKB_COMPOSE_STATE_NO_FLAGS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn global(registry: ?*c.struct_wl_registry, name: u32, interface: []const u8, version: u32) void {
|
||||||
|
const s = state();
|
||||||
|
if (std.mem.eql(u8, interface, "wl_seat") and s.seat == null and version >= 5) {
|
||||||
|
s.seat = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_seat_interface, @min(version, 8)));
|
||||||
|
s.seat_name = name;
|
||||||
|
if (s.seat != null) _ = c.wl_seat_add_listener(s.seat, &seat_listener, null);
|
||||||
|
} else if (std.mem.eql(u8, interface, "wp_cursor_shape_manager_v1")) {
|
||||||
|
s.cursor_manager = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_cursor_shape_manager_v1_interface, 1));
|
||||||
|
} else if (std.mem.eql(u8, interface, "zwp_text_input_manager_v3")) {
|
||||||
|
s.text_manager = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_zwp_text_input_manager_v3_interface, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn setup() void {
|
||||||
|
const s = state();
|
||||||
|
if (s.pointer != null and s.cursor_manager != null and s.cursor_device == null)
|
||||||
|
s.cursor_device = c.wp_cursor_shape_manager_v1_get_pointer(s.cursor_manager, s.pointer);
|
||||||
|
if (s.seat != null and s.text_manager != null and s.text_input == null) {
|
||||||
|
s.text_input = c.zwp_text_input_manager_v3_get_text_input(s.text_manager, s.seat);
|
||||||
|
if (s.text_input != null) Text.listen(s.text_input);
|
||||||
|
}
|
||||||
|
Data.setup();
|
||||||
|
native.setCapability(n.UHDK_CAP_POINTER_CAPTURE, s.pointer != null);
|
||||||
|
native.setCapability(n.UHDK_CAP_TEXT_INPUT, s.context != null and s.keyboard != null);
|
||||||
|
}
|
||||||
|
fn seatCapabilities(_: ?*anyopaque, seat: ?*c.struct_wl_seat, caps: u32) callconv(.c) void {
|
||||||
|
const s = state();
|
||||||
|
if (caps & c.WL_SEAT_CAPABILITY_KEYBOARD != 0 and s.keyboard == null) {
|
||||||
|
s.keyboard = c.wl_seat_get_keyboard(seat);
|
||||||
|
if (s.keyboard != null) _ = c.wl_keyboard_add_listener(s.keyboard, &keyboard_listener, null);
|
||||||
|
} else if (caps & c.WL_SEAT_CAPABILITY_KEYBOARD == 0 and s.keyboard != null) {
|
||||||
|
leaveKeyboard();
|
||||||
|
c.wl_keyboard_release(s.keyboard);
|
||||||
|
s.keyboard = null;
|
||||||
|
}
|
||||||
|
if (caps & c.WL_SEAT_CAPABILITY_POINTER != 0 and s.pointer == null) {
|
||||||
|
s.pointer = c.wl_seat_get_pointer(seat);
|
||||||
|
if (s.pointer != null) _ = c.wl_pointer_add_listener(s.pointer, &pointer_listener, null);
|
||||||
|
} else if (caps & c.WL_SEAT_CAPABILITY_POINTER == 0 and s.pointer != null) {
|
||||||
|
leavePointer();
|
||||||
|
if (s.cursor_device != null) c.wp_cursor_shape_device_v1_destroy(s.cursor_device);
|
||||||
|
s.cursor_device = null;
|
||||||
|
c.wl_pointer_release(s.pointer);
|
||||||
|
s.pointer = null;
|
||||||
|
}
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
fn seatName(_: ?*anyopaque, _: ?*c.struct_wl_seat, _: [*c]const u8) callconv(.c) void {}
|
||||||
|
const seat_listener: c.struct_wl_seat_listener = .{ .capabilities = seatCapabilities, .name = seatName };
|
||||||
|
fn keymap(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, format: u32, fd: i32, size: u32) callconv(.c) void {
|
||||||
|
defer _ = c.close(fd);
|
||||||
|
const s = state();
|
||||||
|
s.repeat_key = null;
|
||||||
|
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
|
if (s.key_focus) |w| Text.cancel(w);
|
||||||
|
if (s.state != null) c.z_xkb_state_unref.?(s.state);
|
||||||
|
if (s.keymap != null) c.z_xkb_keymap_unref.?(s.keymap);
|
||||||
|
s.state = null;
|
||||||
|
s.keymap = null;
|
||||||
|
if (s.context == null or format != c.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 or size == 0 or size > 16 * 1024 * 1024) return;
|
||||||
|
const mapped = c.mmap(null, size, c.PROT_READ, c.MAP_PRIVATE, fd, 0);
|
||||||
|
if (mapped == c.MAP_FAILED) return;
|
||||||
|
defer _ = c.munmap(mapped, size);
|
||||||
|
const bytes: [*]const u8 = @ptrCast(mapped.?);
|
||||||
|
if (bytes[size - 1] != 0) return;
|
||||||
|
s.keymap = c.z_xkb_keymap_new_from_string.?(s.context, @ptrCast(mapped), c.XKB_KEYMAP_FORMAT_TEXT_V1, c.XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||||
|
if (s.keymap != null) s.state = c.z_xkb_state_new.?(s.keymap);
|
||||||
|
}
|
||||||
|
fn keyEnter(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, surface: ?*c.struct_wl_surface, keys: [*c]c.struct_wl_array) callconv(.c) void {
|
||||||
|
leaveKeyboard();
|
||||||
|
const s = state();
|
||||||
|
s.key_focus = find(surface);
|
||||||
|
if (s.state != null and keys.*.size != 0) {
|
||||||
|
const held: [*]const u32 = @ptrCast(@alignCast(keys.*.data));
|
||||||
|
for (held[0 .. keys.*.size / 4]) |code| {
|
||||||
|
if (code < s.held.len) s.held[code] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn keyLeave(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, _: ?*c.struct_wl_surface) callconv(.c) void {
|
||||||
|
leaveKeyboard();
|
||||||
|
}
|
||||||
|
fn updateModifiers() void {
|
||||||
|
const s = state();
|
||||||
|
s.modifiers = 0;
|
||||||
|
if (s.state == null) return;
|
||||||
|
const names = [_][*:0]const u8{ "Shift", "Control", "Mod1", "Mod4", "Lock", "Mod2" };
|
||||||
|
for (names, 0..) |name, index| if (c.z_xkb_state_mod_name_is_active.?(s.state, name, c.XKB_STATE_MODS_EFFECTIVE) > 0) {
|
||||||
|
s.modifiers |= @as(u32, 1) << @intCast(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn modifiers(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, depressed: u32, latched: u32, locked: u32, group: u32) callconv(.c) void {
|
||||||
|
const s = state();
|
||||||
|
if (s.state != null) _ = c.z_xkb_state_update_mask.?(s.state, depressed, latched, locked, 0, 0, group);
|
||||||
|
updateModifiers();
|
||||||
|
}
|
||||||
|
fn key(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, serial: u32, _: u32, code: u32, pressed: u32) callconv(.c) void {
|
||||||
|
const s = state();
|
||||||
|
if (code > 0xffff) return;
|
||||||
|
provenance(serial);
|
||||||
|
if (code < s.held.len) s.held[code] = pressed != 0;
|
||||||
|
// The compositor supplies the authoritative modifier/group mask. Running
|
||||||
|
// XKB key actions locally as well would apply latch/lock actions twice.
|
||||||
|
deliverKey(code, pressed != 0, false);
|
||||||
|
if (pressed != 0 and s.repeat_rate > 0 and s.keymap != null and c.z_xkb_keymap_key_repeats.?(s.keymap, code + 8) != 0) {
|
||||||
|
s.repeat_key = code;
|
||||||
|
s.repeat_at = runtime.now(b().app.io) + @as(u64, s.repeat_delay) * std.time.ns_per_ms;
|
||||||
|
} else if (s.repeat_key == code) s.repeat_key = null;
|
||||||
|
}
|
||||||
|
fn deliverKey(code: u32, down: bool, repeated: bool) void {
|
||||||
|
const s = state();
|
||||||
|
const w = s.key_focus orelse return;
|
||||||
|
const sym = if (s.state != null) c.z_xkb_state_key_get_one_sym.?(s.state, code + 8) else 0;
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_KEY;
|
||||||
|
event.input_id = id();
|
||||||
|
event.data.key = .{ .physical_key = Keyboard.physical(code), .logical_key = if (s.state != null) Keyboard.logical(sym) else 0, .modifiers = s.modifiers, .pressed = @intFromBool(down), .repeat = @intFromBool(repeated) };
|
||||||
|
send(w, &event);
|
||||||
|
if (down and w.text.enabled) Text.key(w, code, sym, event.data.key.logical_key, event.input_id);
|
||||||
|
}
|
||||||
|
fn repeatInfo(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, rate: i32, delay: i32) callconv(.c) void {
|
||||||
|
state().repeat_rate = @intCast(std.math.clamp(rate, 0, 1000));
|
||||||
|
state().repeat_delay = @intCast(@max(0, delay));
|
||||||
|
if (rate <= 0) state().repeat_key = null;
|
||||||
|
}
|
||||||
|
const keyboard_listener: c.struct_wl_keyboard_listener = .{ .keymap = keymap, .enter = keyEnter, .leave = keyLeave, .key = key, .modifiers = modifiers, .repeat_info = repeatInfo };
|
||||||
|
pub fn tick() i32 {
|
||||||
|
const s = state();
|
||||||
|
const active = s.key_focus != null;
|
||||||
|
if (!active) Data.focusLost();
|
||||||
|
if (active != s.active) {
|
||||||
|
s.active = active;
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_APP_ACTIVATION;
|
||||||
|
event.data.active = @intFromBool(active);
|
||||||
|
n.z_emit(runtime.toAppHandle(b().app), &event);
|
||||||
|
}
|
||||||
|
if (s.repeat_key) |code| {
|
||||||
|
const now = runtime.now(b().app.io);
|
||||||
|
if (now >= s.repeat_at) {
|
||||||
|
deliverKey(code, true, true);
|
||||||
|
s.repeat_at = now + std.time.ns_per_s / @max(1, s.repeat_rate);
|
||||||
|
}
|
||||||
|
return @intCast(@min(1000, (s.repeat_at -| now) / std.time.ns_per_ms + 1));
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fn leaveKeyboard() void {
|
||||||
|
const s = state();
|
||||||
|
s.repeat_key = null;
|
||||||
|
if (s.key_focus) |w| {
|
||||||
|
for (&s.held, 0..) |*held, code| if (held.*) {
|
||||||
|
held.* = false;
|
||||||
|
deliverKey(@intCast(code), false, false);
|
||||||
|
};
|
||||||
|
Text.cancel(w);
|
||||||
|
}
|
||||||
|
s.key_focus = null;
|
||||||
|
s.modifiers = 0;
|
||||||
|
s.serial = 0;
|
||||||
|
if (s.state != null) _ = c.z_xkb_state_update_mask.?(s.state, 0, 0, 0, 0, 0, 0);
|
||||||
|
normal();
|
||||||
|
}
|
||||||
|
fn pointerEvent(kind: u32, button_: u32, pressed: bool, delta: n.uhdk_point) void {
|
||||||
|
const s = state();
|
||||||
|
const w = s.pointer_focus orelse return;
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = kind;
|
||||||
|
e.input_id = id();
|
||||||
|
e.data.pointer = .{ .position = s.position, .delta = delta, .button = button_, .pressed = @intFromBool(pressed), .modifiers = s.modifiers, .click_count = if (pressed) s.click_count else 0 };
|
||||||
|
send(w, &e);
|
||||||
|
}
|
||||||
|
fn pointerEnter(_: ?*anyopaque, _: ?*c.struct_wl_pointer, serial: u32, surface: ?*c.struct_wl_surface, x: i32, y: i32) callconv(.c) void {
|
||||||
|
leavePointer();
|
||||||
|
const s = state();
|
||||||
|
s.pointer_focus = find(surface);
|
||||||
|
s.pointer_serial = serial;
|
||||||
|
s.position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
||||||
|
pointerEvent(n.UHDK_EVENT_POINTER_ENTER, 0, false, .{ .x = 0, .y = 0 });
|
||||||
|
if (s.pointer_focus) |w| _ = cursor(w, w.cursor);
|
||||||
|
}
|
||||||
|
fn pointerLeave(_: ?*anyopaque, _: ?*c.struct_wl_pointer, _: u32, _: ?*c.struct_wl_surface) callconv(.c) void {
|
||||||
|
leavePointer();
|
||||||
|
}
|
||||||
|
fn leavePointer() void {
|
||||||
|
const s = state();
|
||||||
|
normal();
|
||||||
|
pointerEvent(n.UHDK_EVENT_POINTER_LEAVE, 0, false, .{ .x = 0, .y = 0 });
|
||||||
|
s.pointer_focus = null;
|
||||||
|
s.buttons = 0;
|
||||||
|
s.pointer_serial = 0;
|
||||||
|
s.scrolling = false;
|
||||||
|
s.axis = .{ .x = 0, .y = 0 };
|
||||||
|
s.wheel = .{ .x = 0, .y = 0 };
|
||||||
|
s.axis_stop = false;
|
||||||
|
s.last_click_button = 0;
|
||||||
|
}
|
||||||
|
fn motion(_: ?*anyopaque, _: ?*c.struct_wl_pointer, _: u32, x: i32, y: i32) callconv(.c) void {
|
||||||
|
const s = state();
|
||||||
|
const previous = s.position;
|
||||||
|
s.position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
||||||
|
pointerEvent(n.UHDK_EVENT_POINTER_MOVE, 0, false, .{ .x = s.position.x - previous.x, .y = s.position.y - previous.y });
|
||||||
|
}
|
||||||
|
fn button(_: ?*anyopaque, _: ?*c.struct_wl_pointer, serial: u32, _: u32, code: u32, pressed: u32) callconv(.c) void {
|
||||||
|
const s = state();
|
||||||
|
provenance(serial);
|
||||||
|
const mapped: u32 = switch (code) {
|
||||||
|
0x110 => n.UHDK_BUTTON_PRIMARY,
|
||||||
|
0x111 => n.UHDK_BUTTON_SECONDARY,
|
||||||
|
0x112 => n.UHDK_BUTTON_MIDDLE,
|
||||||
|
0x113, 0x116 => n.UHDK_BUTTON_BACK,
|
||||||
|
0x114, 0x115 => n.UHDK_BUTTON_FORWARD,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
if (mapped == 0) return;
|
||||||
|
const mask = @as(u32, 1) << @intCast(mapped);
|
||||||
|
if (pressed != 0) {
|
||||||
|
s.buttons |= mask;
|
||||||
|
const now = runtime.now(b().app.io);
|
||||||
|
const dx = s.position.x - s.last_click_position.x;
|
||||||
|
const dy = s.position.y - s.last_click_position.y;
|
||||||
|
s.click_count = if (s.last_click_button == mapped and now -| s.last_click_time <= 500 * std.time.ns_per_ms and dx * dx + dy * dy <= 25) @min(3, s.click_count + 1) else 1;
|
||||||
|
s.last_click_time = now;
|
||||||
|
s.last_click_button = mapped;
|
||||||
|
s.last_click_position = s.position;
|
||||||
|
} else s.buttons &= ~mask;
|
||||||
|
pointerEvent(n.UHDK_EVENT_POINTER_BUTTON, mapped, pressed != 0, .{ .x = 0, .y = 0 });
|
||||||
|
if (s.buttons == 0) normal();
|
||||||
|
}
|
||||||
|
fn axis(_: ?*anyopaque, _: ?*c.struct_wl_pointer, _: u32, which: u32, value: i32) callconv(.c) void {
|
||||||
|
const v: f32 = @as(f32, @floatFromInt(value)) / 256;
|
||||||
|
if (which == c.WL_POINTER_AXIS_VERTICAL_SCROLL) state().axis.y += v else state().axis.x += v;
|
||||||
|
}
|
||||||
|
fn axisSource(_: ?*anyopaque, _: ?*c.struct_wl_pointer, source: u32) callconv(.c) void {
|
||||||
|
state().axis_source = source;
|
||||||
|
}
|
||||||
|
fn axisStop(_: ?*anyopaque, _: ?*c.struct_wl_pointer, _: u32, _: u32) callconv(.c) void {
|
||||||
|
state().axis_stop = true;
|
||||||
|
}
|
||||||
|
fn axisDiscrete(_: ?*anyopaque, _: ?*c.struct_wl_pointer, which: u32, value: i32) callconv(.c) void {
|
||||||
|
if (c.wl_pointer_get_version(state().pointer) >= 8) return;
|
||||||
|
if (which == c.WL_POINTER_AXIS_VERTICAL_SCROLL) state().wheel.y += @floatFromInt(value) else state().wheel.x += @floatFromInt(value);
|
||||||
|
}
|
||||||
|
fn axis120(_: ?*anyopaque, _: ?*c.struct_wl_pointer, which: u32, value: i32) callconv(.c) void {
|
||||||
|
if (which == c.WL_POINTER_AXIS_VERTICAL_SCROLL) state().wheel.y += @as(f32, @floatFromInt(value)) / 120 else state().wheel.x += @as(f32, @floatFromInt(value)) / 120;
|
||||||
|
}
|
||||||
|
fn frame(_: ?*anyopaque, _: ?*c.struct_wl_pointer) callconv(.c) void {
|
||||||
|
const s = state();
|
||||||
|
defer {
|
||||||
|
s.axis = .{ .x = 0, .y = 0 };
|
||||||
|
s.wheel = .{ .x = 0, .y = 0 };
|
||||||
|
s.axis_stop = false;
|
||||||
|
}
|
||||||
|
const w = s.pointer_focus orelse return;
|
||||||
|
const discrete = s.axis_source == c.WL_POINTER_AXIS_SOURCE_WHEEL or s.axis_source == c.WL_POINTER_AXIS_SOURCE_WHEEL_TILT;
|
||||||
|
const has_wheel = s.wheel.x != 0 or s.wheel.y != 0;
|
||||||
|
if (s.axis.x == 0 and s.axis.y == 0 and !has_wheel and !s.axis_stop) return;
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = n.UHDK_EVENT_SCROLL;
|
||||||
|
e.input_id = id();
|
||||||
|
e.data.scroll = .{ .position = s.position, .delta = if (has_wheel) s.wheel else s.axis, .unit = if (has_wheel) n.UHDK_SCROLL_LINES else n.UHDK_SCROLL_LOGICAL_PIXELS, .phase = if (discrete) n.UHDK_SCROLL_DISCRETE else if (s.axis_stop) n.UHDK_SCROLL_END else if (!s.scrolling) n.UHDK_SCROLL_BEGIN else n.UHDK_SCROLL_UPDATE, .modifiers = s.modifiers, .momentum = 0 };
|
||||||
|
s.scrolling = !discrete and !s.axis_stop;
|
||||||
|
send(w, &e);
|
||||||
|
}
|
||||||
|
const pointer_listener: c.struct_wl_pointer_listener = .{ .enter = pointerEnter, .leave = pointerLeave, .motion = motion, .button = button, .axis = axis, .frame = frame, .axis_source = axisSource, .axis_stop = axisStop, .axis_discrete = axisDiscrete, .axis_value120 = axis120, .axis_relative_direction = null };
|
||||||
|
pub fn cursor(w: *native.Window, shape: u32) u32 {
|
||||||
|
w.cursor = shape;
|
||||||
|
const s = state();
|
||||||
|
if (shape != n.UHDK_CURSOR_HIDDEN and s.cursor_device == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
if (s.pointer_focus != w) return abi.OK;
|
||||||
|
if (shape == n.UHDK_CURSOR_HIDDEN) {
|
||||||
|
c.wl_pointer_set_cursor(s.pointer, s.pointer_serial, null, 0, 0);
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
if (s.cursor_device == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
const shapes = [_]u32{ c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT };
|
||||||
|
c.wp_cursor_shape_device_v1_set_shape(s.cursor_device, s.pointer_serial, shapes[shape]);
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
pub fn capture(w: *native.Window, enabled: bool) u32 {
|
||||||
|
const s = state();
|
||||||
|
if (enabled and (s.pointer_focus != w or s.buttons == 0)) return abi.UNAVAILABLE;
|
||||||
|
if (!enabled) {
|
||||||
|
normal();
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
s.captured = true;
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = n.UHDK_EVENT_POINTER_MODE_CHANGED;
|
||||||
|
e.data.pointer_mode = n.UHDK_POINTER_CAPTURE;
|
||||||
|
send(w, &e);
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
fn normal() void {
|
||||||
|
const s = state();
|
||||||
|
if (!s.captured) return;
|
||||||
|
s.captured = false;
|
||||||
|
if (s.pointer_focus) |w| {
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = n.UHDK_EVENT_POINTER_MODE_CHANGED;
|
||||||
|
e.data.pointer_mode = n.UHDK_POINTER_NORMAL;
|
||||||
|
send(w, &e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn forget(w: *native.Window) void {
|
||||||
|
if (state().key_focus == w) leaveKeyboard();
|
||||||
|
if (state().pointer_focus == w) leavePointer();
|
||||||
|
Text.forget(w);
|
||||||
|
}
|
||||||
|
pub fn remove(name: u32) void {
|
||||||
|
if (name == state().seat_name) releaseSeat();
|
||||||
|
}
|
||||||
|
fn releaseSeat() void {
|
||||||
|
Data.releaseSeat();
|
||||||
|
const s = state();
|
||||||
|
leaveKeyboard();
|
||||||
|
leavePointer();
|
||||||
|
if (s.text_focus) |w| Text.forget(w);
|
||||||
|
if (s.text_input != null) c.zwp_text_input_v3_destroy(s.text_input);
|
||||||
|
if (s.cursor_device != null) c.wp_cursor_shape_device_v1_destroy(s.cursor_device);
|
||||||
|
if (s.keyboard != null) c.wl_keyboard_release(s.keyboard);
|
||||||
|
if (s.pointer != null) c.wl_pointer_release(s.pointer);
|
||||||
|
if (s.seat != null) c.wl_seat_release(s.seat);
|
||||||
|
s.text_input = null;
|
||||||
|
s.cursor_device = null;
|
||||||
|
s.keyboard = null;
|
||||||
|
s.pointer = null;
|
||||||
|
s.seat = null;
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
pub fn deinit() void {
|
||||||
|
const s = state();
|
||||||
|
releaseSeat();
|
||||||
|
if (s.text_manager != null) c.zwp_text_input_manager_v3_destroy(s.text_manager);
|
||||||
|
if (s.cursor_manager != null) c.wp_cursor_shape_manager_v1_destroy(s.cursor_manager);
|
||||||
|
if (s.compose != null) c.z_xkb_compose_state_unref.?(s.compose);
|
||||||
|
if (s.compose_table != null) c.z_xkb_compose_table_unref.?(s.compose_table);
|
||||||
|
if (s.state != null) c.z_xkb_state_unref.?(s.state);
|
||||||
|
if (s.keymap != null) c.z_xkb_keymap_unref.?(s.keymap);
|
||||||
|
if (s.context != null) c.z_xkb_context_unref.?(s.context);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
|
||||||
|
/// evdev keyboard codes to USB HID keyboard-page usages. Values describe
|
||||||
|
/// physical positions, independently of the compositor-supplied XKB layout.
|
||||||
|
pub fn physical(code: u32) u32 {
|
||||||
|
const map = comptime blk: {
|
||||||
|
var t = [_]u16{0} ** 256;
|
||||||
|
const pairs = [_][2]u16{
|
||||||
|
.{ 1, 41 }, .{ 12, 45 }, .{ 13, 46 }, .{ 14, 42 }, .{ 15, 43 }, .{ 26, 47 }, .{ 27, 48 }, .{ 28, 40 }, .{ 29, 224 },
|
||||||
|
.{ 39, 51 }, .{ 40, 52 }, .{ 41, 53 }, .{ 42, 225 }, .{ 43, 49 }, .{ 51, 54 }, .{ 52, 55 }, .{ 53, 56 }, .{ 54, 229 },
|
||||||
|
.{ 55, 85 }, .{ 56, 226 }, .{ 57, 44 }, .{ 58, 57 }, .{ 69, 83 }, .{ 70, 71 }, .{ 71, 95 }, .{ 72, 96 }, .{ 73, 97 },
|
||||||
|
.{ 74, 86 }, .{ 75, 92 }, .{ 76, 93 }, .{ 77, 94 }, .{ 78, 87 }, .{ 79, 89 }, .{ 80, 90 }, .{ 81, 91 }, .{ 82, 98 },
|
||||||
|
.{ 83, 99 }, .{ 86, 100 }, .{ 87, 68 }, .{ 88, 69 }, .{ 96, 88 }, .{ 97, 228 }, .{ 98, 84 }, .{ 99, 70 }, .{ 100, 230 },
|
||||||
|
.{ 102, 74 }, .{ 103, 82 }, .{ 104, 75 }, .{ 105, 80 }, .{ 106, 79 }, .{ 107, 77 }, .{ 108, 81 }, .{ 109, 78 }, .{ 110, 73 },
|
||||||
|
.{ 111, 76 }, .{ 119, 72 }, .{ 125, 227 }, .{ 126, 231 }, .{ 127, 101 },
|
||||||
|
};
|
||||||
|
for (pairs) |p| t[p[0]] = p[1];
|
||||||
|
for (0..10) |i| t[2 + i] = 30 + i;
|
||||||
|
for (0..10) |i| t[59 + i] = 58 + i;
|
||||||
|
for (0..12) |i| t[183 + i] = 104 + i;
|
||||||
|
for ("qwertyuiop", 16..) |letter, code_| t[code_] = letter - 'a' + 4;
|
||||||
|
for ("asdfghjkl", 30..) |letter, code_| t[code_] = letter - 'a' + 4;
|
||||||
|
for ("zxcvbnm", 44..) |letter, code_| t[code_] = letter - 'a' + 4;
|
||||||
|
break :blk t;
|
||||||
|
};
|
||||||
|
return if (code < map.len) map[code] else 0;
|
||||||
|
}
|
||||||
|
pub fn logical(sym: u32) u32 {
|
||||||
|
return switch (sym) {
|
||||||
|
c.XKB_KEY_Escape => n.UHDK_KEY_ESCAPE,
|
||||||
|
c.XKB_KEY_Return, c.XKB_KEY_KP_Enter => n.UHDK_KEY_ENTER,
|
||||||
|
c.XKB_KEY_Tab, c.XKB_KEY_ISO_Left_Tab => n.UHDK_KEY_TAB,
|
||||||
|
c.XKB_KEY_BackSpace => n.UHDK_KEY_BACKSPACE,
|
||||||
|
c.XKB_KEY_Delete, c.XKB_KEY_KP_Delete => n.UHDK_KEY_DELETE,
|
||||||
|
c.XKB_KEY_Insert, c.XKB_KEY_KP_Insert => n.UHDK_KEY_INSERT,
|
||||||
|
c.XKB_KEY_Left, c.XKB_KEY_KP_Left => n.UHDK_KEY_LEFT,
|
||||||
|
c.XKB_KEY_Right, c.XKB_KEY_KP_Right => n.UHDK_KEY_RIGHT,
|
||||||
|
c.XKB_KEY_Up, c.XKB_KEY_KP_Up => n.UHDK_KEY_UP,
|
||||||
|
c.XKB_KEY_Down, c.XKB_KEY_KP_Down => n.UHDK_KEY_DOWN,
|
||||||
|
c.XKB_KEY_Home, c.XKB_KEY_KP_Home => n.UHDK_KEY_HOME,
|
||||||
|
c.XKB_KEY_End, c.XKB_KEY_KP_End => n.UHDK_KEY_END,
|
||||||
|
c.XKB_KEY_Page_Up, c.XKB_KEY_KP_Page_Up => n.UHDK_KEY_PAGE_UP,
|
||||||
|
c.XKB_KEY_Page_Down, c.XKB_KEY_KP_Page_Down => n.UHDK_KEY_PAGE_DOWN,
|
||||||
|
c.XKB_KEY_F1...c.XKB_KEY_F24 => @as(u32, n.UHDK_KEY_F1) + sym - c.XKB_KEY_F1,
|
||||||
|
else => c.z_xkb_keysym_to_utf32.?(sym),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn edit(key: u32, mods: u32) u32 {
|
||||||
|
const ctrl = mods & n.UHDK_MOD_CONTROL != 0;
|
||||||
|
if (mods & (n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return 0;
|
||||||
|
if (ctrl) switch (key) {
|
||||||
|
'c', 'C' => return n.UHDK_EDIT_COPY,
|
||||||
|
'x', 'X' => return n.UHDK_EDIT_CUT,
|
||||||
|
'v', 'V' => return n.UHDK_EDIT_PASTE,
|
||||||
|
'a', 'A' => return n.UHDK_EDIT_SELECT_ALL,
|
||||||
|
'z', 'Z' => return if (mods & n.UHDK_MOD_SHIFT != 0) n.UHDK_EDIT_REDO else n.UHDK_EDIT_UNDO,
|
||||||
|
'y', 'Y' => return n.UHDK_EDIT_REDO,
|
||||||
|
else => {},
|
||||||
|
};
|
||||||
|
return switch (key) {
|
||||||
|
n.UHDK_KEY_BACKSPACE => n.UHDK_EDIT_DELETE_BACKWARD,
|
||||||
|
n.UHDK_KEY_DELETE => n.UHDK_EDIT_DELETE_FORWARD,
|
||||||
|
n.UHDK_KEY_LEFT => if (ctrl) n.UHDK_EDIT_WORD_LEFT else n.UHDK_EDIT_MOVE_LEFT,
|
||||||
|
n.UHDK_KEY_RIGHT => if (ctrl) n.UHDK_EDIT_WORD_RIGHT else n.UHDK_EDIT_MOVE_RIGHT,
|
||||||
|
n.UHDK_KEY_UP => n.UHDK_EDIT_MOVE_UP,
|
||||||
|
n.UHDK_KEY_DOWN => n.UHDK_EDIT_MOVE_DOWN,
|
||||||
|
n.UHDK_KEY_HOME => if (ctrl) n.UHDK_EDIT_DOCUMENT_START else n.UHDK_EDIT_LINE_START,
|
||||||
|
n.UHDK_KEY_END => if (ctrl) n.UHDK_EDIT_DOCUMENT_END else n.UHDK_EDIT_LINE_END,
|
||||||
|
n.UHDK_KEY_ENTER => if (ctrl) 0 else n.UHDK_EDIT_NEWLINE,
|
||||||
|
n.UHDK_KEY_TAB => if (ctrl) 0 else n.UHDK_EDIT_TAB,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
test "physical positions and editor shortcuts" {
|
||||||
|
try std.testing.expectEqual(@as(u32, 4), physical(30));
|
||||||
|
try std.testing.expectEqual(@as(u32, 29), physical(44));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), physical(500));
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_EDIT_COPY), edit('c', n.UHDK_MOD_CONTROL));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), edit('c', n.UHDK_MOD_CONTROL | n.UHDK_MOD_ALT));
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_EDIT_DOCUMENT_START), edit(n.UHDK_KEY_HOME, n.UHDK_MOD_CONTROL));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,512 @@
|
||||||
|
//! Freedesktop portal frontend over a private, asynchronously pumped D-Bus
|
||||||
|
//! connection. The session chooses its backend (including KDE's native dialogs).
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Files = @import("Files.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const desktop = "org.freedesktop.portal.Desktop";
|
||||||
|
const path = "/org/freedesktop/portal/desktop";
|
||||||
|
const chooser = "org.freedesktop.portal.FileChooser";
|
||||||
|
const settings = "org.freedesktop.portal.Settings";
|
||||||
|
const dbus = "org.freedesktop.DBus";
|
||||||
|
const dbus_path = "/org/freedesktop/DBus";
|
||||||
|
const Pending = struct {
|
||||||
|
next: ?*Pending = null,
|
||||||
|
id: u64,
|
||||||
|
command: ?*const n.z_command,
|
||||||
|
serial: u32 = 0,
|
||||||
|
replied: bool = false,
|
||||||
|
cancelled: bool = false,
|
||||||
|
handle: [:0]u8,
|
||||||
|
token: [:0]u8,
|
||||||
|
exported: ?*c.struct_zxdg_exported_v2 = null,
|
||||||
|
deadline: u64,
|
||||||
|
};
|
||||||
|
pub const State = struct {
|
||||||
|
connection: ?*c.DBusConnection = null,
|
||||||
|
unique: ?[:0]u8 = null,
|
||||||
|
owner: ?[:0]u8 = null,
|
||||||
|
hello: u32 = 0,
|
||||||
|
matches: [3]u32 = @splat(0),
|
||||||
|
ready: bool = false,
|
||||||
|
version_serial: u32 = 0,
|
||||||
|
settings_serial: u32 = 0,
|
||||||
|
chooser_version: u32 = 0,
|
||||||
|
startup_deadline: u64 = 0,
|
||||||
|
pending: ?*Pending = null,
|
||||||
|
exporter: ?*c.struct_zxdg_exporter_v2 = null,
|
||||||
|
preferences: n.uhdk_preferences = .{ .struct_size = @sizeOf(n.uhdk_preferences), .appearance = n.UHDK_APPEARANCE_UNKNOWN, .reduced_motion = 0, .high_contrast = 0 },
|
||||||
|
};
|
||||||
|
fn b() *native.Backend {
|
||||||
|
return native.instance.?;
|
||||||
|
}
|
||||||
|
fn s() *State {
|
||||||
|
return &b().portal;
|
||||||
|
}
|
||||||
|
fn send(message: *Bus.Message) !u32 {
|
||||||
|
var serial: u32 = 0;
|
||||||
|
if (c.z_dbus_connection_send.?(s().connection, message, &serial) == 0) return error.OutOfMemory;
|
||||||
|
return serial;
|
||||||
|
}
|
||||||
|
pub fn global(registry: ?*c.struct_wl_registry, name: u32, interface: []const u8, _: u32) void {
|
||||||
|
if (std.mem.eql(u8, interface, "zxdg_exporter_v2")) s().exporter = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_zxdg_exporter_v2_interface, 1));
|
||||||
|
}
|
||||||
|
pub fn initialize() void {
|
||||||
|
if (c.z_linux_dbus_available() == 0 or c.z_dbus_threads_init_default.?() == 0) return;
|
||||||
|
// Avoid libdbus autolaunch/process spawning. Desktop sessions provide this
|
||||||
|
// address; absence disables portals without affecting windows or input.
|
||||||
|
const address = c.getenv("DBUS_SESSION_BUS_ADDRESS");
|
||||||
|
if (address == null or !std.mem.startsWith(u8, std.mem.span(address), "unix:")) return;
|
||||||
|
s().connection = c.z_dbus_connection_open_private.?(address, null) orelse return;
|
||||||
|
c.z_dbus_connection_set_exit_on_disconnect.?(s().connection, 0);
|
||||||
|
s().startup_deadline = runtime.now(b().app.io) + 5 * std.time.ns_per_s;
|
||||||
|
const message = Bus.method(dbus, dbus_path, dbus, "Hello") catch {
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer c.z_dbus_message_unref.?(message);
|
||||||
|
s().hello = send(message) catch {
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn addMatches() !void {
|
||||||
|
const rules = [_][*:0]const u8{
|
||||||
|
"type='signal',sender='org.freedesktop.portal.Desktop',interface='org.freedesktop.portal.Request',member='Response'",
|
||||||
|
"type='signal',sender='org.freedesktop.portal.Desktop',interface='org.freedesktop.portal.Settings',member='SettingChanged',path='/org/freedesktop/portal/desktop'",
|
||||||
|
"type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.freedesktop.portal.Desktop'",
|
||||||
|
};
|
||||||
|
for (rules, 0..) |rule, i| {
|
||||||
|
const message = try Bus.method(dbus, dbus_path, dbus, "AddMatch");
|
||||||
|
defer c.z_dbus_message_unref.?(message);
|
||||||
|
var args = Bus.append(message);
|
||||||
|
try Bus.string(&args, rule);
|
||||||
|
s().matches[i] = try send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn discover() !void {
|
||||||
|
const version = try Bus.method(desktop, path, "org.freedesktop.DBus.Properties", "Get");
|
||||||
|
defer c.z_dbus_message_unref.?(version);
|
||||||
|
var args = Bus.append(version);
|
||||||
|
try Bus.string(&args, chooser);
|
||||||
|
try Bus.string(&args, "version");
|
||||||
|
s().version_serial = try send(version);
|
||||||
|
const read = try Bus.method(desktop, path, settings, "ReadAll");
|
||||||
|
defer c.z_dbus_message_unref.?(read);
|
||||||
|
args = Bus.append(read);
|
||||||
|
var namespaces = try Bus.open(&args, c.DBUS_TYPE_ARRAY, "s");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&args, &namespaces);
|
||||||
|
try Bus.string(&namespaces, "org.freedesktop.appearance");
|
||||||
|
try Bus.close(&args, &namespaces);
|
||||||
|
s().settings_serial = try send(read);
|
||||||
|
}
|
||||||
|
pub fn poll(fds: *std.ArrayList(c.struct_pollfd)) !void {
|
||||||
|
const connection = s().connection orelse return;
|
||||||
|
var fd: c_int = -1;
|
||||||
|
if (c.z_dbus_connection_get_unix_fd.?(connection, &fd) != 0)
|
||||||
|
try fds.append(b().app.gpa, .{ .fd = fd, .events = @as(c_short, c.POLLIN) | @as(c_short, if (c.z_dbus_connection_has_messages_to_send.?(connection) != 0) c.POLLOUT else 0), .revents = 0 });
|
||||||
|
}
|
||||||
|
pub fn tick() i32 {
|
||||||
|
const connection = s().connection orelse return -1;
|
||||||
|
if (c.z_dbus_connection_read_write.?(connection, 0) == 0) {
|
||||||
|
disconnect();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Bound dispatch work so an active bus cannot starve Wayland or app commands.
|
||||||
|
for (0..64) |_| {
|
||||||
|
const message = c.z_dbus_connection_pop_message.?(connection) orelse break;
|
||||||
|
defer c.z_dbus_message_unref.?(message);
|
||||||
|
process(message) catch {
|
||||||
|
disconnect();
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
if (s().connection == null) return -1;
|
||||||
|
} else return 0;
|
||||||
|
const now = runtime.now(b().app.io);
|
||||||
|
if (!s().ready and now >= s().startup_deadline) {
|
||||||
|
disconnect();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |pending| {
|
||||||
|
cursor = pending.next;
|
||||||
|
if (pending.deadline != 0 and now >= pending.deadline) {
|
||||||
|
closeRequest(pending.handle);
|
||||||
|
complete(pending, abi.UNAVAILABLE, null);
|
||||||
|
destroy(pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (s().pending != null or !s().ready) 100 else -1;
|
||||||
|
}
|
||||||
|
fn setOwner(message: *Bus.Message) !void {
|
||||||
|
const sender = c.z_dbus_message_get_sender.?(message);
|
||||||
|
if (sender == null) return;
|
||||||
|
if (s().owner) |old| b().app.gpa.free(old);
|
||||||
|
s().owner = try b().app.gpa.dupeZ(u8, std.mem.span(sender));
|
||||||
|
}
|
||||||
|
fn process(message: *Bus.Message) !void {
|
||||||
|
const serial = c.z_dbus_message_get_reply_serial.?(message);
|
||||||
|
const failed = c.z_dbus_message_get_type.?(message) == c.DBUS_MESSAGE_TYPE_ERROR;
|
||||||
|
var args = Bus.parse(message);
|
||||||
|
if (serial != 0 and serial == s().hello) {
|
||||||
|
s().hello = 0;
|
||||||
|
if (failed or c.z_dbus_message_has_signature.?(message, "s") == 0) {
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s().unique = try b().app.gpa.dupeZ(u8, Bus.str(&args).?);
|
||||||
|
try addMatches();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (&s().matches) |*match| if (serial != 0 and serial == match.*) {
|
||||||
|
if (failed) {
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match.* = 0;
|
||||||
|
if (std.mem.allEqual(u32, &s().matches, 0)) {
|
||||||
|
s().ready = true;
|
||||||
|
try discover();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (serial != 0 and serial == s().version_serial) {
|
||||||
|
s().version_serial = 0;
|
||||||
|
if (!failed) {
|
||||||
|
var value = Bus.variant(&args) orelse return;
|
||||||
|
s().chooser_version = Bus.uint(&value) orelse 0;
|
||||||
|
try setOwner(message);
|
||||||
|
native.setCapability(n.UHDK_CAP_FILE_DIALOGS, s().chooser_version >= 3);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (serial != 0 and serial == s().settings_serial) {
|
||||||
|
s().settings_serial = 0;
|
||||||
|
if (!failed and c.z_dbus_message_has_signature.?(message, "a{sa{sv}}") != 0) {
|
||||||
|
try setOwner(message);
|
||||||
|
var appearance = Bus.entry(&args, "org.freedesktop.appearance") orelse return;
|
||||||
|
inline for (.{ "color-scheme", "contrast", "reduced-motion" }) |key| {
|
||||||
|
if (Bus.entry(&appearance, key)) |raw| {
|
||||||
|
var v = raw;
|
||||||
|
if (Bus.variant(&v)) |inner| {
|
||||||
|
var value = inner;
|
||||||
|
preference(key, Bus.uint(&value) orelse 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.z_dbus_message_is_signal.?(message, dbus, "NameOwnerChanged") != 0 and c.z_dbus_message_has_signature.?(message, "sss") != 0) {
|
||||||
|
const sender = c.z_dbus_message_get_sender.?(message);
|
||||||
|
if (sender == null or !std.mem.eql(u8, std.mem.span(sender), dbus)) return;
|
||||||
|
if (!std.mem.eql(u8, Bus.str(&args).?, desktop)) return;
|
||||||
|
_ = Bus.next(&args);
|
||||||
|
const old = Bus.str(&args).?;
|
||||||
|
_ = Bus.next(&args);
|
||||||
|
const new = Bus.str(&args).?;
|
||||||
|
if (old.len != 0) {
|
||||||
|
while (s().pending) |p| {
|
||||||
|
complete(p, abi.UNAVAILABLE, null);
|
||||||
|
destroy(p);
|
||||||
|
}
|
||||||
|
s().chooser_version = 0;
|
||||||
|
native.setCapability(n.UHDK_CAP_FILE_DIALOGS, false);
|
||||||
|
if (s().owner) |owner| b().app.gpa.free(owner);
|
||||||
|
s().owner = null;
|
||||||
|
preference("color-scheme", 0);
|
||||||
|
preference("contrast", 0);
|
||||||
|
preference("reduced-motion", 0);
|
||||||
|
}
|
||||||
|
if (new.len != 0) {
|
||||||
|
if (s().owner) |owner| b().app.gpa.free(owner);
|
||||||
|
s().owner = try b().app.gpa.dupeZ(u8, new);
|
||||||
|
try discover();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |pending| : (cursor = pending.next) {
|
||||||
|
if (serial != 0 and serial == pending.serial) {
|
||||||
|
pending.replied = true;
|
||||||
|
if (failed) {
|
||||||
|
complete(pending, errorStatus(message), null);
|
||||||
|
destroy(pending);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.z_dbus_message_has_signature.?(message, "o") == 0) {
|
||||||
|
complete(pending, n.UHDK_STATUS_OS_ERROR, null);
|
||||||
|
destroy(pending);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handle = try b().app.gpa.dupeZ(u8, Bus.str(&args).?);
|
||||||
|
b().app.gpa.free(pending.handle);
|
||||||
|
pending.handle = handle;
|
||||||
|
if (pending.id == 0) {
|
||||||
|
if (pending.cancelled) closeRequest(pending.handle);
|
||||||
|
destroy(pending);
|
||||||
|
} else pending.deadline = 0; // Human interaction has no artificial timeout.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Match rules are installed on the bus, and sender validation also protects
|
||||||
|
// against unrelated unicast signals sent directly to our private connection.
|
||||||
|
const sender = c.z_dbus_message_get_sender.?(message);
|
||||||
|
if (sender == null or s().owner == null or !std.mem.eql(u8, std.mem.span(sender), s().owner.?)) return;
|
||||||
|
if (c.z_dbus_message_is_signal.?(message, settings, "SettingChanged") != 0 and c.z_dbus_message_has_signature.?(message, "ssv") != 0) {
|
||||||
|
if (!std.mem.eql(u8, Bus.str(&args).?, "org.freedesktop.appearance")) return;
|
||||||
|
_ = Bus.next(&args);
|
||||||
|
const key = Bus.str(&args).?;
|
||||||
|
_ = Bus.next(&args);
|
||||||
|
var value = Bus.variant(&args).?;
|
||||||
|
preference(key, Bus.uint(&value) orelse 0);
|
||||||
|
} else if (c.z_dbus_message_is_signal.?(message, "org.freedesktop.portal.Request", "Response") != 0 and c.z_dbus_message_has_signature.?(message, "ua{sv}") != 0) {
|
||||||
|
const response_path = c.z_dbus_message_get_path.?(message);
|
||||||
|
if (response_path == null) return;
|
||||||
|
cursor = s().pending;
|
||||||
|
while (cursor) |pending| : (cursor = pending.next) if (std.mem.eql(u8, pending.handle, std.mem.span(response_path))) {
|
||||||
|
const code = Bus.uint(&args).?;
|
||||||
|
_ = Bus.next(&args);
|
||||||
|
if (pending.id != 0) {
|
||||||
|
if (code != 0) complete(pending, if (code == 1) abi.CANCELLED else n.UHDK_STATUS_OS_ERROR, null) else if (pending.command.?.op == n.Z_URL) complete(pending, abi.OK, null) else responseFiles(pending, &args);
|
||||||
|
}
|
||||||
|
if (pending.replied) destroy(pending);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn errorStatus(message: *Bus.Message) u32 {
|
||||||
|
const name = c.z_dbus_message_get_error_name.?(message);
|
||||||
|
if (name != null and (std.mem.endsWith(u8, std.mem.span(name), ".NotFound") or std.mem.endsWith(u8, std.mem.span(name), ".ServiceUnknown") or std.mem.endsWith(u8, std.mem.span(name), ".UnknownMethod"))) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
}
|
||||||
|
fn preference(key: []const u8, value: u32) void {
|
||||||
|
const old = s().preferences;
|
||||||
|
if (std.mem.eql(u8, key, "color-scheme")) s().preferences.appearance = switch (value) {
|
||||||
|
1 => n.UHDK_APPEARANCE_DARK,
|
||||||
|
2 => n.UHDK_APPEARANCE_LIGHT,
|
||||||
|
else => n.UHDK_APPEARANCE_UNKNOWN,
|
||||||
|
} else if (std.mem.eql(u8, key, "contrast")) s().preferences.high_contrast = @intFromBool(value == 1) else if (std.mem.eql(u8, key, "reduced-motion")) s().preferences.reduced_motion = @intFromBool(value == 1);
|
||||||
|
if (!std.meta.eql(old, s().preferences)) n.z_preferences(runtime.toAppHandle(b().app), &s().preferences);
|
||||||
|
}
|
||||||
|
pub fn request(cmd: *const n.z_command, id: u64) u32 {
|
||||||
|
if (!s().ready or s().connection == null) return abi.UNAVAILABLE;
|
||||||
|
if (cmd.op == n.Z_DIALOG and s().chooser_version < 3) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
const p = allocate(cmd, id) catch return abi.OOM;
|
||||||
|
p.next = s().pending;
|
||||||
|
s().pending = p;
|
||||||
|
if (cmd.op == n.Z_DIALOG and cmd.dialog.parent != null and s().exporter != null) {
|
||||||
|
const w: *native.Window = @ptrCast(@alignCast(n.z_object_native(cmd.dialog.parent) orelse {
|
||||||
|
destroy(p);
|
||||||
|
return abi.UNAVAILABLE;
|
||||||
|
}));
|
||||||
|
p.exported = c.zxdg_exporter_v2_export_toplevel(s().exporter, w.surface) orelse {
|
||||||
|
destroy(p);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
_ = c.zxdg_exported_v2_add_listener(p.exported, &export_listener, p);
|
||||||
|
} else submit(p, "") catch {
|
||||||
|
destroy(p);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
return n.Z_DEFERRED;
|
||||||
|
}
|
||||||
|
fn allocate(cmd: *const n.z_command, id: u64) !*Pending {
|
||||||
|
const gpa = b().app.gpa;
|
||||||
|
const p = try gpa.create(Pending);
|
||||||
|
errdefer gpa.destroy(p);
|
||||||
|
const token = try std.fmt.allocPrintSentinel(gpa, "uhdk_{x}", .{id}, 0);
|
||||||
|
errdefer gpa.free(token);
|
||||||
|
const unique = s().unique.?;
|
||||||
|
const name = try gpa.dupe(u8, unique[1..]);
|
||||||
|
defer gpa.free(name);
|
||||||
|
std.mem.replaceScalar(u8, name, '.', '_');
|
||||||
|
p.* = .{ .id = id, .command = cmd, .token = token, .handle = try std.fmt.allocPrintSentinel(gpa, path ++ "/request/{s}/{s}", .{ name, token }, 0), .deadline = runtime.now(b().app.io) + 30 * std.time.ns_per_s };
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
fn exported(raw: ?*anyopaque, _: ?*c.struct_zxdg_exported_v2, handle: [*c]const u8) callconv(.c) void {
|
||||||
|
const p: *Pending = @ptrCast(@alignCast(raw.?));
|
||||||
|
const parent = std.fmt.allocPrintSentinel(b().app.gpa, "wayland:{s}", .{std.mem.span(handle)}, 0) catch {
|
||||||
|
complete(p, abi.OOM, null);
|
||||||
|
destroy(p);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer b().app.gpa.free(parent);
|
||||||
|
submit(p, parent) catch {
|
||||||
|
complete(p, abi.OOM, null);
|
||||||
|
destroy(p);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const export_listener: c.struct_zxdg_exported_v2_listener = .{ .handle = exported };
|
||||||
|
fn submit(p: *Pending, parent: [*:0]const u8) !void {
|
||||||
|
const cmd = p.command.?;
|
||||||
|
const dialog = cmd.op == n.Z_DIALOG;
|
||||||
|
const message = try Bus.method(desktop, path, if (dialog) chooser else "org.freedesktop.portal.OpenURI", if (!dialog) "OpenURI" else if (cmd.dialog.kind == n.UHDK_DIALOG_SAVE_FILE) "SaveFile" else "OpenFile");
|
||||||
|
defer c.z_dbus_message_unref.?(message);
|
||||||
|
var arena: std.heap.ArenaAllocator = .init(b().app.gpa);
|
||||||
|
defer arena.deinit();
|
||||||
|
const gpa = arena.allocator();
|
||||||
|
var args = Bus.append(message);
|
||||||
|
try Bus.string(&args, parent);
|
||||||
|
try Bus.string(&args, try gpa.dupeZ(u8, abi.textBytes(if (dialog) cmd.dialog.title else cmd.text)));
|
||||||
|
var options = try Bus.open(&args, c.DBUS_TYPE_ARRAY, "{sv}");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&args, &options);
|
||||||
|
try Bus.option(&options, "handle_token", "s", p.token.ptr);
|
||||||
|
if (dialog) {
|
||||||
|
try Bus.option(&options, "modal", "b", @as(u32, @intFromBool(cmd.dialog.parent != null)));
|
||||||
|
if (cmd.dialog.kind != n.UHDK_DIALOG_SAVE_FILE) {
|
||||||
|
try Bus.option(&options, "multiple", "b", @as(u32, @intFromBool(cmd.dialog.kind == n.UHDK_DIALOG_OPEN_FILES)));
|
||||||
|
try Bus.option(&options, "directory", "b", @as(u32, @intFromBool(cmd.dialog.kind == n.UHDK_DIALOG_OPEN_DIRECTORY)));
|
||||||
|
}
|
||||||
|
if (cmd.dialog.accept_label.len != 0) try Bus.option(&options, "accept_label", "s", (try gpa.dupeZ(u8, abi.textBytes(cmd.dialog.accept_label))).ptr);
|
||||||
|
if (cmd.dialog.suggested_name.len != 0 and cmd.dialog.kind == n.UHDK_DIALOG_SAVE_FILE) try Bus.option(&options, "current_name", "s", (try gpa.dupeZ(u8, abi.textBytes(cmd.dialog.suggested_name))).ptr);
|
||||||
|
if (cmd.dialog.initial_directory != null) {
|
||||||
|
const directory = abi.bytes(n.z_file_path(cmd.dialog.initial_directory));
|
||||||
|
var entry = try Bus.open(&options, c.DBUS_TYPE_DICT_ENTRY, null);
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&options, &entry);
|
||||||
|
try Bus.string(&entry, "current_folder");
|
||||||
|
var variant = try Bus.open(&entry, c.DBUS_TYPE_VARIANT, "ay");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&entry, &variant);
|
||||||
|
var bytes = try Bus.open(&variant, c.DBUS_TYPE_ARRAY, "y");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&variant, &bytes);
|
||||||
|
for (directory) |byte| try Bus.basic(&bytes, c.DBUS_TYPE_BYTE, byte);
|
||||||
|
try Bus.basic(&bytes, c.DBUS_TYPE_BYTE, @as(u8, 0));
|
||||||
|
try Bus.close(&variant, &bytes);
|
||||||
|
try Bus.close(&entry, &variant);
|
||||||
|
try Bus.close(&options, &entry);
|
||||||
|
}
|
||||||
|
if (cmd.dialog.filters_len != 0) try filters(&options, cmd.dialog, gpa);
|
||||||
|
}
|
||||||
|
try Bus.close(&args, &options);
|
||||||
|
p.serial = try send(message);
|
||||||
|
}
|
||||||
|
fn filters(options: *Bus.Iter, desc: n.uhdk_file_dialog_desc, gpa: std.mem.Allocator) !void {
|
||||||
|
var entry = try Bus.open(options, c.DBUS_TYPE_DICT_ENTRY, null);
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(options, &entry);
|
||||||
|
try Bus.string(&entry, "filters");
|
||||||
|
var variant = try Bus.open(&entry, c.DBUS_TYPE_VARIANT, "a(sa(us))");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&entry, &variant);
|
||||||
|
var list = try Bus.open(&variant, c.DBUS_TYPE_ARRAY, "(sa(us))");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&variant, &list);
|
||||||
|
for (desc.filters[0..@intCast(desc.filters_len)]) |filter| {
|
||||||
|
var pair = try Bus.open(&list, c.DBUS_TYPE_STRUCT, null);
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&list, &pair);
|
||||||
|
try Bus.string(&pair, try gpa.dupeZ(u8, abi.textBytes(filter.label)));
|
||||||
|
var patterns = try Bus.open(&pair, c.DBUS_TYPE_ARRAY, "(us)");
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&pair, &patterns);
|
||||||
|
for (0..@max(1, filter.extensions.len)) |index| {
|
||||||
|
const pattern: [:0]const u8 = if (filter.extensions.len == 0) "*" else try std.fmt.allocPrintSentinel(gpa, "*.{s}", .{abi.textBytes(filter.extensions.ptr[index])}, 0);
|
||||||
|
var rule = try Bus.open(&patterns, c.DBUS_TYPE_STRUCT, null);
|
||||||
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&patterns, &rule);
|
||||||
|
try Bus.basic(&rule, c.DBUS_TYPE_UINT32, @as(u32, 0));
|
||||||
|
try Bus.string(&rule, pattern);
|
||||||
|
try Bus.close(&patterns, &rule);
|
||||||
|
}
|
||||||
|
try Bus.close(&pair, &patterns);
|
||||||
|
try Bus.close(&list, &pair);
|
||||||
|
}
|
||||||
|
try Bus.close(&variant, &list);
|
||||||
|
try Bus.close(&entry, &variant);
|
||||||
|
try Bus.close(options, &entry);
|
||||||
|
}
|
||||||
|
fn responseFiles(p: *Pending, args: *Bus.Iter) void {
|
||||||
|
var raw = Bus.entry(args, "uris") orelse {
|
||||||
|
complete(p, n.UHDK_STATUS_OS_ERROR, null);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var value = Bus.variant(&raw) orelse {
|
||||||
|
complete(p, n.UHDK_STATUS_OS_ERROR, null);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (Bus.kind(&value) != c.DBUS_TYPE_ARRAY) {
|
||||||
|
complete(p, n.UHDK_STATUS_OS_ERROR, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var strings = Bus.child(&value);
|
||||||
|
var files: std.ArrayList(?*n.uhdk_file) = .empty;
|
||||||
|
defer {
|
||||||
|
for (files.items) |file| n.uhdk_file_release(file);
|
||||||
|
files.deinit(b().app.gpa);
|
||||||
|
}
|
||||||
|
while (Bus.kind(&strings) != c.DBUS_TYPE_INVALID) {
|
||||||
|
const uri = Bus.str(&strings) orelse {
|
||||||
|
complete(p, n.UHDK_STATUS_OS_ERROR, null);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const bytes = Files.decode(b().app.gpa, uri) catch |err| {
|
||||||
|
complete(p, if (err == error.OutOfMemory) abi.OOM else n.UHDK_STATUS_UNSUPPORTED, null);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer b().app.gpa.free(bytes);
|
||||||
|
const file = n.z_file_from_url(runtime.toAppHandle(b().app), .{ .ptr = bytes.ptr, .len = bytes.len }, null) orelse {
|
||||||
|
complete(p, abi.OOM, null);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
files.append(b().app.gpa, file) catch {
|
||||||
|
n.uhdk_file_release(file);
|
||||||
|
complete(p, abi.OOM, null);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (!Bus.next(&strings)) break;
|
||||||
|
}
|
||||||
|
var result = std.mem.zeroes(n.z_request_result);
|
||||||
|
result.value.files = .{ .ptr = files.items.ptr, .len = files.items.len };
|
||||||
|
complete(p, if (files.items.len == 0) abi.CANCELLED else abi.OK, &result);
|
||||||
|
}
|
||||||
|
fn complete(p: *Pending, status: u32, result: ?*n.z_request_result) void {
|
||||||
|
if (p.id != 0) n.z_complete(runtime.toAppHandle(b().app), p.id, status, result);
|
||||||
|
p.id = 0;
|
||||||
|
p.command = null;
|
||||||
|
}
|
||||||
|
fn closeRequest(handle: [*:0]const u8) void {
|
||||||
|
if (s().connection == null) return;
|
||||||
|
const message = Bus.method(desktop, handle, "org.freedesktop.portal.Request", "Close") catch return;
|
||||||
|
defer c.z_dbus_message_unref.?(message);
|
||||||
|
c.z_dbus_message_set_no_reply.?(message, 1);
|
||||||
|
_ = send(message) catch {};
|
||||||
|
}
|
||||||
|
pub fn cancel(id: u64) bool {
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |p| : (cursor = p.next) if (p.id == id) {
|
||||||
|
if (p.serial != 0) closeRequest(p.handle);
|
||||||
|
p.cancelled = true;
|
||||||
|
complete(p, abi.CANCELLED, null);
|
||||||
|
if (p.serial == 0 or p.replied) destroy(p);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fn destroy(p: *Pending) void {
|
||||||
|
var link = &s().pending;
|
||||||
|
while (link.* != p) link = &link.*.?.next;
|
||||||
|
link.* = p.next;
|
||||||
|
if (p.exported != null) c.zxdg_exported_v2_destroy(p.exported);
|
||||||
|
b().app.gpa.free(p.handle);
|
||||||
|
b().app.gpa.free(p.token);
|
||||||
|
b().app.gpa.destroy(p);
|
||||||
|
}
|
||||||
|
fn disconnect() void {
|
||||||
|
while (s().pending) |p| {
|
||||||
|
complete(p, abi.UNAVAILABLE, null);
|
||||||
|
destroy(p);
|
||||||
|
}
|
||||||
|
if (s().connection) |connection| {
|
||||||
|
c.z_dbus_connection_close.?(connection);
|
||||||
|
c.z_dbus_connection_unref.?(connection);
|
||||||
|
}
|
||||||
|
s().connection = null;
|
||||||
|
s().ready = false;
|
||||||
|
native.setCapability(n.UHDK_CAP_FILE_DIALOGS, false);
|
||||||
|
}
|
||||||
|
pub fn deinit() void {
|
||||||
|
var cursor = s().pending;
|
||||||
|
while (cursor) |p| : (cursor = p.next) closeRequest(p.handle);
|
||||||
|
disconnect();
|
||||||
|
if (s().unique) |unique| b().app.gpa.free(unique);
|
||||||
|
if (s().owner) |owner| b().app.gpa.free(owner);
|
||||||
|
if (s().exporter != null) c.zxdg_exporter_v2_destroy(s().exporter);
|
||||||
|
}
|
||||||
|
|
@ -4,12 +4,14 @@ The first implementation targets KDE Plasma Wayland using standard protocols.
|
||||||
It implements window creation/destruction, titles, parent relationships, size
|
It implements window creation/destruction, titles, parent relationships, size
|
||||||
constraints, maximize/fullscreen/minimize requests, show/hide, close requests,
|
constraints, maximize/fullscreen/minimize requests, show/hide, close requests,
|
||||||
logical/pixel metrics, EGL desktop OpenGL, and local POSIX file references.
|
logical/pixel metrics, EGL desktop OpenGL, and local POSIX file references.
|
||||||
|
Keyboard/pointer input, cursor shapes, XKB composition, text-input-v3 IME,
|
||||||
|
clipboard, incoming file drops, portal dialogs/URLs/preferences, activation,
|
||||||
|
application icons, and inotify file watches are also implemented.
|
||||||
|
|
||||||
Keyboard/pointer input, cursors, text input/IME, clipboard, drag and drop,
|
Native menus, a cursor-theme fallback, client-side window decorations, and X11
|
||||||
portals, preferences, file watches, native menus, icons and activation requests
|
remain unsupported. Protocol tests cover IME batching; a live Fcitx/IBus CJK
|
||||||
are not implemented yet. Their capabilities are false and requests return
|
session and other desktops still need qualification. Missing optional protocols
|
||||||
`UNSUPPORTED`. X11 is also unsupported. This is a window/graphics foundation,
|
or libraries disable their features without a desktop-name whitelist.
|
||||||
not yet a complete interactive desktop backend.
|
|
||||||
|
|
||||||
## Build and run
|
## Build and run
|
||||||
|
|
||||||
|
|
@ -28,6 +30,8 @@ C files and translates the private headers at build time.
|
||||||
|
|
||||||
At runtime, UHDK opens `libwayland-client.so.0`. OpenGL also needs
|
At runtime, UHDK opens `libwayland-client.so.0`. OpenGL also needs
|
||||||
`libwayland-egl.so.1`, `libEGL.so.1` and a working system graphics driver.
|
`libwayland-egl.so.1`, `libEGL.so.1` and a working system graphics driver.
|
||||||
|
Keyboard layout/text processing needs `libxkbcommon.so.0`. Portal services need
|
||||||
|
`libdbus-1.so.3`, a session bus, and the desktop portal frontend/backend.
|
||||||
Missing EGL disables the OpenGL capability without preventing window creation.
|
Missing EGL disables the OpenGL capability without preventing window creation.
|
||||||
No desktop libraries are linked or bundled. EGL 1.5 and desktop GL 3.3 core are
|
No desktop libraries are linked or bundled. EGL 1.5 and desktop GL 3.3 core are
|
||||||
required for graphics; there is no OpenGL ES fallback. A non-null capability
|
required for graphics; there is no OpenGL ES fallback. A non-null capability
|
||||||
|
|
@ -43,7 +47,8 @@ failure requests shutdown and cancels pending work; it never switches backends.
|
||||||
|
|
||||||
NixOS does not normally expose every desktop library through the conventional
|
NixOS does not normally expose every desktop library through the conventional
|
||||||
runtime linker search path. The optional package-owned shell supplies the pinned
|
runtime linker search path. The optional package-owned shell supplies the pinned
|
||||||
scanner, Python, clang-format and runtime library paths:
|
scanner, Python (including dbus-next for fixtures), dbus-daemon, clang-format
|
||||||
|
and runtime library paths:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix-shell
|
nix-shell
|
||||||
|
|
@ -87,7 +92,8 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
||||||
application's show/hide intent, not proof that the compositor displays it.
|
application's show/hide intent, not proof that the compositor displays it.
|
||||||
- Size/state commands acknowledge submission. Confirmed geometry and focus arrive
|
- Size/state commands acknowledge submission. Confirmed geometry and focus arrive
|
||||||
through metrics. Wayland cannot report minimized state, force focus, or directly
|
through metrics. Wayland cannot report minimized state, force focus, or directly
|
||||||
undo minimization. Activation tokens will be added separately.
|
undo minimization. Activation requests use a launch token or a recent focused
|
||||||
|
input serial; the compositor can decline to focus the window.
|
||||||
- Fractional scale uses fractional-scale-v1 plus viewporter; otherwise preferred
|
- Fractional scale uses fractional-scale-v1 plus viewporter; otherwise preferred
|
||||||
integer buffer scale is used when wl_surface v6 is available. Older compositors
|
integer buffer scale is used when wl_surface v6 is available. Older compositors
|
||||||
without those mechanisms currently use scale 1. Fractional/mixed-DPI behavior
|
without those mechanisms currently use scale 1. Fractional/mixed-DPI behavior
|
||||||
|
|
@ -104,6 +110,47 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
||||||
context. Window framebuffer dimensions describe the presentation target;
|
context. Window framebuffer dimensions describe the presentation target;
|
||||||
applications continue to own their viewport and rendering resources.
|
applications continue to own their viewport and rendering resources.
|
||||||
|
|
||||||
|
## Input and desktop services
|
||||||
|
|
||||||
|
- One active seat owns keyboard, pointer, and clipboard state. The keyboard uses
|
||||||
|
the compositor's XKB layout/masks, USB HID physical usages, client-side repeat,
|
||||||
|
and locale-aware compose tables. Focus loss releases tracked keys and stops repeat.
|
||||||
|
- Text input uses text-input-v3 when available and local XKB composition for
|
||||||
|
forwarded keys. IME batches retain their surrounding-text revision and UTF-8
|
||||||
|
offsets until `done`. Surrounding text is cropped to the protocol's 4000-byte
|
||||||
|
limit; selections too large to fit disable surrounding-text edits for that
|
||||||
|
snapshot. Ctrl editing shortcuts produce native edit events.
|
||||||
|
- Pointer motion, buttons, wheel/touchpad frames and cursor-shape-v1 are supported.
|
||||||
|
Capture acknowledges Wayland's implicit button grab during a selection drag;
|
||||||
|
requesting it without a held button/focused pointer returns `UNAVAILABLE`.
|
||||||
|
The last button release or focus loss restores normal mode. There is no pointer
|
||||||
|
lock, relative motion, touch, tablet, or themed-cursor fallback yet.
|
||||||
|
- Clipboard writes require keyboard focus and a triggering input serial. They
|
||||||
|
never wait for an unrelated future key. Reads negotiate UTF-8/plain text,
|
||||||
|
enforce the caller's byte limit without truncation, and reject invalid UTF-8.
|
||||||
|
Transfers use nonblocking reads/writes with ten-second deadlines and preserve
|
||||||
|
source/offer ownership across cancellation and selection changes. Incoming
|
||||||
|
drops accept local `text/uri-list` files using copy-only negotiation, bounded
|
||||||
|
to 16 MiB. Remote/KIO URIs are not local file references.
|
||||||
|
- File dialogs and URL opening use `org.freedesktop.portal.Desktop`, never KDE's
|
||||||
|
backend service directly. Dialogs export their parent through xdg-foreign-v2,
|
||||||
|
support filters, initial directories, suggested names and multiple selection.
|
||||||
|
A private D-Bus connection subscribes before submission, correlates early
|
||||||
|
responses, closes cancelled requests, and handles portal-owner changes.
|
||||||
|
Portal discovery is asynchronous, so its capability can appear after `READY`.
|
||||||
|
Connection/method setup is bounded; human interaction has no timeout.
|
||||||
|
- Portal Settings supplies appearance, contrast and reduced-motion preferences
|
||||||
|
and live changes. Unknown/unavailable values retain neutral defaults. See the
|
||||||
|
[Settings contract](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html)
|
||||||
|
and [request lifecycle](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html).
|
||||||
|
- Icons use xdg-toplevel-icon with square, premultiplied ARGB shared-memory
|
||||||
|
buffers; rectangular public images receive transparent padding. App IDs and
|
||||||
|
installed desktop entries still govern normal desktop application identity.
|
||||||
|
- File watches use inotify. A document watch follows its named path through
|
||||||
|
atomic replacement by observing its parent. Directory watches observe immediate
|
||||||
|
children. Queue overflow requests a rescan; lost roots are invalidated. Each
|
||||||
|
watch owns an inotify descriptor and is subject to the host's instance limits.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
From the package directory:
|
From the package directory:
|
||||||
|
|
@ -115,6 +162,10 @@ zig build test-linux-native # opens windows in the active session
|
||||||
zig build linux-test-bin
|
zig build linux-test-bin
|
||||||
python3 src/linux/tests/startup.py zig-out/bin/linux-native-tests
|
python3 src/linux/tests/startup.py zig-out/bin/linux-native-tests
|
||||||
python3 src/linux/tests/disconnect.py zig-out/bin/linux-native-tests
|
python3 src/linux/tests/disconnect.py zig-out/bin/linux-native-tests
|
||||||
|
# In the optional nix-shell, or with dbus-next/dbus-daemon installed:
|
||||||
|
python3 src/linux/tests/wayland_fixture.py zig-out/bin/linux-service-tests
|
||||||
|
python3 src/linux/tests/portal_fixture.py zig-out/bin/linux-service-tests
|
||||||
|
zig-out/bin/linux-service-tests desktop # live portal discovery/icons/file watches
|
||||||
python3 src/linux/tools/protocols.py --check
|
python3 src/linux/tools/protocols.py --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -124,3 +175,13 @@ actual GL version, pixel readback and byte-preserving file URIs. The startup tes
|
||||||
uses isolated sockets for absent, disconnected and unresponsive servers. The
|
uses isolated sockets for absent, disconnected and unresponsive servers. The
|
||||||
runtime-disconnect test proxies and closes only its own client connection; it
|
runtime-disconnect test proxies and closes only its own client connection; it
|
||||||
does not stop or modify the desktop compositor.
|
does not stop or modify the desktop compositor.
|
||||||
|
|
||||||
|
The service fixtures own their compositor socket and D-Bus daemon. They exercise
|
||||||
|
keys/repeat/dead-key composition, IME revisions/deletion/preedit, scrolling,
|
||||||
|
clipboard size limits and early reader exit, URI drops, activation, portal
|
||||||
|
filters/results, response-before-reply, cancellation with a late alternate
|
||||||
|
handle, parent destruction and portal restart. They never open a real chooser
|
||||||
|
or browser or inject input into the user's desktop. The live service mode checks
|
||||||
|
portal discovery, icon submission and atomic-save watch behavior without touching
|
||||||
|
the user's clipboard. These fixtures supplement live desktop testing; they do
|
||||||
|
not establish compatibility with every compositor or input-method engine.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,337 @@
|
||||||
|
//! IME batches are applied at done, using the snapshot associated with its
|
||||||
|
//! commit serial. Forwarded wl_keyboard keys use local XKB composition.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const Input = @import("Input.zig");
|
||||||
|
const Keyboard = @import("Keyboard.zig");
|
||||||
|
|
||||||
|
const Sent = struct {
|
||||||
|
next: ?*Sent = null,
|
||||||
|
serial: u32,
|
||||||
|
revision: u64,
|
||||||
|
bytes: [:0]u8,
|
||||||
|
offset: usize,
|
||||||
|
start: usize,
|
||||||
|
len: usize,
|
||||||
|
};
|
||||||
|
pub const State = struct {
|
||||||
|
enabled: bool = false,
|
||||||
|
composing: bool = false,
|
||||||
|
ime_composing: bool = false,
|
||||||
|
snapshot: n.uhdk_text_state = std.mem.zeroes(n.uhdk_text_state),
|
||||||
|
surrounding: []u8 = &.{},
|
||||||
|
last_edit: u64 = 0,
|
||||||
|
sent: ?*Sent = null,
|
||||||
|
sent_count: usize = 0,
|
||||||
|
dirty: bool = true,
|
||||||
|
pending_preedit: ?[]u8 = null,
|
||||||
|
pending_commit: ?[]u8 = null,
|
||||||
|
cursor_begin: i32 = 0,
|
||||||
|
cursor_end: i32 = 0,
|
||||||
|
before: u32 = 0,
|
||||||
|
after: u32 = 0,
|
||||||
|
};
|
||||||
|
pub fn listen(input: ?*c.struct_zwp_text_input_v3) void {
|
||||||
|
_ = c.zwp_text_input_v3_add_listener(input, &listener, null);
|
||||||
|
}
|
||||||
|
fn commit() u32 {
|
||||||
|
const s = Input.state();
|
||||||
|
s.text_serial +%= 1;
|
||||||
|
c.zwp_text_input_v3_commit(s.text_input);
|
||||||
|
return s.text_serial;
|
||||||
|
}
|
||||||
|
fn enter(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, surface: ?*c.struct_wl_surface) callconv(.c) void {
|
||||||
|
const s = Input.state();
|
||||||
|
s.text_focus = Input.find(surface);
|
||||||
|
if (s.text_focus) |w| if (w.text.enabled) {
|
||||||
|
c.zwp_text_input_v3_enable(s.text_input);
|
||||||
|
_ = publish(w);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn leave(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, _: ?*c.struct_wl_surface) callconv(.c) void {
|
||||||
|
if (Input.state().text_focus) |w| {
|
||||||
|
end(w);
|
||||||
|
clearPending(w);
|
||||||
|
clearSent(w);
|
||||||
|
}
|
||||||
|
Input.state().text_focus = null;
|
||||||
|
}
|
||||||
|
fn own(w: *native.Window, destination: *?[]u8, text: [*c]const u8) void {
|
||||||
|
if (destination.*) |old| w.owner.gpa.free(old);
|
||||||
|
const bytes = if (text == null) "" else std.mem.span(text);
|
||||||
|
destination.* = w.owner.gpa.dupe(u8, bytes) catch null;
|
||||||
|
}
|
||||||
|
fn preedit(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, text: [*c]const u8, first: i32, last: i32) callconv(.c) void {
|
||||||
|
const w = Input.state().text_focus orelse return;
|
||||||
|
own(w, &w.text.pending_preedit, text);
|
||||||
|
w.text.cursor_begin = first;
|
||||||
|
w.text.cursor_end = last;
|
||||||
|
}
|
||||||
|
fn committed(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, text: [*c]const u8) callconv(.c) void {
|
||||||
|
const w = Input.state().text_focus orelse return;
|
||||||
|
own(w, &w.text.pending_commit, text);
|
||||||
|
}
|
||||||
|
fn deleted(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, before: u32, after: u32) callconv(.c) void {
|
||||||
|
const w = Input.state().text_focus orelse return;
|
||||||
|
w.text.before = before;
|
||||||
|
w.text.after = after;
|
||||||
|
}
|
||||||
|
fn done(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, serial: u32) callconv(.c) void {
|
||||||
|
const w = Input.state().text_focus orelse return;
|
||||||
|
const t = &w.text;
|
||||||
|
defer clearPending(w);
|
||||||
|
if (!t.enabled) return;
|
||||||
|
var sent = t.sent;
|
||||||
|
while (sent) |value| {
|
||||||
|
if (value.serial == serial) break;
|
||||||
|
sent = value.next;
|
||||||
|
}
|
||||||
|
// A cleared serial belongs to a cancelled/disabled input session.
|
||||||
|
const basis = sent orelse return;
|
||||||
|
if (t.pending_commit) |bytes| if (!std.unicode.utf8ValidateSlice(bytes)) return;
|
||||||
|
end(w);
|
||||||
|
if (t.pending_commit != null or t.before != 0 or t.after != 0) {
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_TEXT_COMMIT;
|
||||||
|
event.input_id = Input.id();
|
||||||
|
event.data.text.text = abi.textSpan(t.pending_commit orelse "");
|
||||||
|
event.data.text.revision = basis.revision;
|
||||||
|
if (t.before != 0 or t.after != 0) {
|
||||||
|
if (t.before > basis.start or t.after > basis.bytes.len - basis.start - basis.len) return;
|
||||||
|
const start = basis.start - t.before;
|
||||||
|
const length = t.before + basis.len + t.after;
|
||||||
|
if (!abi.boundary(basis.bytes, start) or !abi.boundary(basis.bytes, start + length)) return;
|
||||||
|
event.data.text.has_replacement = 1;
|
||||||
|
event.data.text.replacement_start = basis.offset + start;
|
||||||
|
event.data.text.replacement_len = length;
|
||||||
|
}
|
||||||
|
t.last_edit = event.input_id;
|
||||||
|
Input.send(w, &event);
|
||||||
|
}
|
||||||
|
if (t.pending_preedit) |bytes| {
|
||||||
|
if (bytes.len != 0 and std.unicode.utf8ValidateSlice(bytes)) {
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_TEXT_PREEDIT;
|
||||||
|
event.input_id = Input.id();
|
||||||
|
event.data.text.text = abi.textSpan(bytes);
|
||||||
|
event.data.text.revision = basis.revision;
|
||||||
|
const lo: usize = @intCast(@max(0, @min(t.cursor_begin, t.cursor_end)));
|
||||||
|
const hi: usize = @intCast(@max(0, @max(t.cursor_begin, t.cursor_end)));
|
||||||
|
if (lo <= hi and hi <= bytes.len and abi.boundary(bytes, lo) and abi.boundary(bytes, hi)) {
|
||||||
|
event.data.text.selection_start = lo;
|
||||||
|
event.data.text.selection_len = hi - lo;
|
||||||
|
}
|
||||||
|
t.composing = true;
|
||||||
|
t.ime_composing = true;
|
||||||
|
t.last_edit = event.input_id;
|
||||||
|
Input.send(w, &event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep the acknowledged basis: more than one done can share its serial.
|
||||||
|
while (t.sent != basis) {
|
||||||
|
const old = t.sent.?;
|
||||||
|
t.sent = old.next;
|
||||||
|
w.owner.gpa.free(old.bytes);
|
||||||
|
w.owner.gpa.destroy(old);
|
||||||
|
t.sent_count -= 1;
|
||||||
|
}
|
||||||
|
// Never send pending state in response to an older serial. A later matching
|
||||||
|
// done or an acknowledged application basis will release the throttle.
|
||||||
|
if (serial == Input.state().text_serial and t.dirty and t.snapshot.acknowledged_input_id >= t.last_edit) _ = publish(w);
|
||||||
|
}
|
||||||
|
const listener: c.struct_zwp_text_input_v3_listener = .{ .enter = enter, .leave = leave, .preedit_string = preedit, .commit_string = committed, .delete_surrounding_text = deleted, .done = done };
|
||||||
|
|
||||||
|
pub fn crop(bytes: []const u8, start: usize, length: usize) ?struct { start: usize, end: usize } {
|
||||||
|
if (length > 4000 or std.mem.findScalar(u8, bytes, 0) != null) return null;
|
||||||
|
var lo = start -| ((4000 - length) / 2);
|
||||||
|
while (lo < start and !abi.boundary(bytes, lo)) lo += 1;
|
||||||
|
var hi = @min(bytes.len, lo + 4000);
|
||||||
|
while (hi > start + length and !abi.boundary(bytes, hi)) hi -= 1;
|
||||||
|
if (hi < start + length) return null;
|
||||||
|
return .{ .start = lo, .end = hi };
|
||||||
|
}
|
||||||
|
fn publish(w: *native.Window) u32 {
|
||||||
|
const s = Input.state();
|
||||||
|
const t = &w.text;
|
||||||
|
if (s.text_input == null or s.text_focus != w or !t.enabled) return abi.OK;
|
||||||
|
// Bound unacknowledged snapshots without discarding snapshots a late IME
|
||||||
|
// batch may still refer to. Keep the newest application state for later.
|
||||||
|
if (t.sent_count >= 32) {
|
||||||
|
t.dirty = true;
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
const range = crop(t.surrounding, @intCast(t.snapshot.selection_start), @intCast(t.snapshot.selection_len));
|
||||||
|
const sent = w.owner.gpa.create(Sent) catch return abi.OOM;
|
||||||
|
const bytes = w.owner.gpa.dupeZ(u8, if (range) |r| t.surrounding[r.start..r.end] else "") catch {
|
||||||
|
w.owner.gpa.destroy(sent);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
sent.* = .{ .serial = s.text_serial +% 1, .revision = t.snapshot.revision, .bytes = bytes, .offset = if (range) |r| r.start else 0, .start = if (range) |r| @intCast(t.snapshot.selection_start - r.start) else 0, .len = if (range != null) @intCast(t.snapshot.selection_len) else 0 };
|
||||||
|
c.zwp_text_input_v3_set_surrounding_text(s.text_input, bytes, @intCast(sent.start + sent.len), @intCast(sent.start));
|
||||||
|
const caret = t.snapshot.caret;
|
||||||
|
c.zwp_text_input_v3_set_cursor_rectangle(s.text_input, @intFromFloat(std.math.clamp(caret.x, -1000000, 1000000)), @intFromFloat(std.math.clamp(caret.y, -1000000, 1000000)), @intFromFloat(@min(caret.width, 1000000)), @intFromFloat(@min(caret.height, 1000000)));
|
||||||
|
c.zwp_text_input_v3_set_content_type(s.text_input, c.ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE, c.ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
|
||||||
|
c.zwp_text_input_v3_set_text_change_cause(s.text_input, if (t.snapshot.acknowledged_input_id == t.last_edit) c.ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD else c.ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER);
|
||||||
|
_ = commit();
|
||||||
|
var link = &t.sent;
|
||||||
|
while (link.*) |item| link = &item.next;
|
||||||
|
link.* = sent;
|
||||||
|
t.sent_count += 1;
|
||||||
|
t.dirty = false;
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
pub fn snapshot(w: *native.Window, value: n.uhdk_text_state) u32 {
|
||||||
|
const t = &w.text;
|
||||||
|
if (value.acknowledged_input_id < t.last_edit) return abi.OK;
|
||||||
|
const bytes = abi.textBytes(value.surrounding_text);
|
||||||
|
const same = t.snapshot.revision == value.revision and t.snapshot.selection_start == value.selection_start and t.snapshot.selection_len == value.selection_len and
|
||||||
|
std.meta.eql(t.snapshot.caret, value.caret) and std.mem.eql(u8, t.surrounding, bytes);
|
||||||
|
const copy = w.owner.gpa.dupe(u8, bytes) catch return abi.OOM;
|
||||||
|
w.owner.gpa.free(t.surrounding);
|
||||||
|
t.surrounding = copy;
|
||||||
|
t.snapshot = value;
|
||||||
|
t.snapshot.surrounding_text = abi.textSpan(copy);
|
||||||
|
if (same and !t.dirty) return abi.OK;
|
||||||
|
t.dirty = true;
|
||||||
|
return publish(w);
|
||||||
|
}
|
||||||
|
pub fn enable(w: *native.Window, enabled: bool) u32 {
|
||||||
|
const t = &w.text;
|
||||||
|
if (enabled and Input.state().context == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
if (t.enabled == enabled) return abi.OK;
|
||||||
|
end(w);
|
||||||
|
clearPending(w);
|
||||||
|
clearSent(w);
|
||||||
|
if (Input.state().compose != null) c.z_xkb_compose_state_reset.?(Input.state().compose);
|
||||||
|
t.enabled = enabled;
|
||||||
|
const s = Input.state();
|
||||||
|
if (s.text_input != null and s.text_focus == w) {
|
||||||
|
if (enabled) {
|
||||||
|
c.zwp_text_input_v3_enable(s.text_input);
|
||||||
|
return publish(w);
|
||||||
|
}
|
||||||
|
c.zwp_text_input_v3_disable(s.text_input);
|
||||||
|
_ = commit();
|
||||||
|
}
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
fn end(w: *native.Window) void {
|
||||||
|
if (w.text.composing) endAt(w, Input.id());
|
||||||
|
}
|
||||||
|
fn endAt(w: *native.Window, input_id: u64) void {
|
||||||
|
const t = &w.text;
|
||||||
|
if (!t.composing) return;
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = n.UHDK_EVENT_TEXT_COMPOSITION_END;
|
||||||
|
e.input_id = input_id;
|
||||||
|
t.last_edit = input_id;
|
||||||
|
Input.send(w, &e);
|
||||||
|
t.composing = false;
|
||||||
|
t.ime_composing = false;
|
||||||
|
}
|
||||||
|
fn clearPending(w: *native.Window) void {
|
||||||
|
const t = &w.text;
|
||||||
|
if (t.pending_preedit) |p| w.owner.gpa.free(p);
|
||||||
|
if (t.pending_commit) |p| w.owner.gpa.free(p);
|
||||||
|
t.pending_preedit = null;
|
||||||
|
t.pending_commit = null;
|
||||||
|
t.before = 0;
|
||||||
|
t.after = 0;
|
||||||
|
}
|
||||||
|
fn clearSent(w: *native.Window) void {
|
||||||
|
while (w.text.sent) |item| {
|
||||||
|
w.text.sent = item.next;
|
||||||
|
w.owner.gpa.free(item.bytes);
|
||||||
|
w.owner.gpa.destroy(item);
|
||||||
|
}
|
||||||
|
w.text.sent_count = 0;
|
||||||
|
w.text.dirty = true;
|
||||||
|
}
|
||||||
|
pub fn cancel(w: *native.Window) void {
|
||||||
|
end(w);
|
||||||
|
clearPending(w);
|
||||||
|
clearSent(w);
|
||||||
|
const s = Input.state();
|
||||||
|
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
|
if (s.text_input != null and s.text_focus == w and w.text.enabled) {
|
||||||
|
c.zwp_text_input_v3_disable(s.text_input);
|
||||||
|
_ = commit();
|
||||||
|
c.zwp_text_input_v3_enable(s.text_input);
|
||||||
|
_ = publish(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn forget(w: *native.Window) void {
|
||||||
|
if (Input.state().text_focus == w) {
|
||||||
|
_ = enable(w, false);
|
||||||
|
Input.state().text_focus = null;
|
||||||
|
}
|
||||||
|
end(w);
|
||||||
|
clearPending(w);
|
||||||
|
clearSent(w);
|
||||||
|
}
|
||||||
|
pub fn deinit(w: *native.Window) void {
|
||||||
|
forget(w);
|
||||||
|
w.owner.gpa.free(w.text.surrounding);
|
||||||
|
}
|
||||||
|
pub fn key(w: *native.Window, code: u32, sym: u32, logical: u32, input_id: u64) void {
|
||||||
|
const s = Input.state();
|
||||||
|
const t = &w.text;
|
||||||
|
if (t.ime_composing or s.state == null) return;
|
||||||
|
const edit = Keyboard.edit(logical, s.modifiers);
|
||||||
|
if (edit != 0) {
|
||||||
|
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
|
endAt(w, input_id);
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = n.UHDK_EVENT_EDIT_COMMAND;
|
||||||
|
e.input_id = input_id;
|
||||||
|
e.data.edit = .{ .command = edit, .extend_selection = @intFromBool(s.modifiers & n.UHDK_MOD_SHIFT != 0) };
|
||||||
|
t.last_edit = input_id;
|
||||||
|
Input.send(w, &e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.modifiers & (n.UHDK_MOD_CONTROL | n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return;
|
||||||
|
var buffer: [256]u8 = undefined;
|
||||||
|
var length: c_int = 0;
|
||||||
|
if (s.compose != null) {
|
||||||
|
_ = c.z_xkb_compose_state_feed.?(s.compose, sym);
|
||||||
|
switch (c.z_xkb_compose_state_get_status.?(s.compose)) {
|
||||||
|
c.XKB_COMPOSE_COMPOSING => {
|
||||||
|
t.composing = true;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
c.XKB_COMPOSE_CANCELLED => {
|
||||||
|
c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
|
endAt(w, input_id);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
c.XKB_COMPOSE_COMPOSED => {
|
||||||
|
length = c.z_xkb_compose_state_get_utf8.?(s.compose, &buffer, buffer.len);
|
||||||
|
c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
|
endAt(w, input_id);
|
||||||
|
},
|
||||||
|
else => length = c.z_xkb_state_key_get_utf8.?(s.state, code + 8, &buffer, buffer.len),
|
||||||
|
}
|
||||||
|
} else length = c.z_xkb_state_key_get_utf8.?(s.state, code + 8, &buffer, buffer.len);
|
||||||
|
if (length <= 0 or length >= buffer.len or buffer[0] < 32 or buffer[0] == 127) return;
|
||||||
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
e.type = n.UHDK_EVENT_TEXT_COMMIT;
|
||||||
|
e.input_id = input_id;
|
||||||
|
e.data.text.text = abi.textSpan(buffer[0..@intCast(length)]);
|
||||||
|
e.data.text.revision = t.snapshot.revision;
|
||||||
|
t.last_edit = input_id;
|
||||||
|
Input.send(w, &e);
|
||||||
|
}
|
||||||
|
test "surrounding crop preserves UTF-8 and complete selections" {
|
||||||
|
var bytes: [8000]u8 = undefined;
|
||||||
|
for (0..4000) |i| {
|
||||||
|
bytes[i * 2] = 0xc3;
|
||||||
|
bytes[i * 2 + 1] = 0xa9;
|
||||||
|
}
|
||||||
|
const range = crop(&bytes, 3998, 4).?;
|
||||||
|
try std.testing.expect(range.start % 2 == 0 and range.end % 2 == 0 and range.end - range.start <= 4000);
|
||||||
|
try std.testing.expect(range.start <= 3998 and range.end >= 4002);
|
||||||
|
try std.testing.expect(crop(&bytes, 0, 4002) == null);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
//! inotify watches observe a document's parent so atomic replacement keeps the
|
||||||
|
//! watch attached to the name. Directory watches cover immediate children only.
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("linux_c");
|
||||||
|
const n = @import("native");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
pub const Watch = struct {
|
||||||
|
next: ?*Watch = null,
|
||||||
|
handle: ?*n.uhdk_watch,
|
||||||
|
root: ?*n.uhdk_file,
|
||||||
|
fd: c_int,
|
||||||
|
directory: []u8,
|
||||||
|
name: []u8,
|
||||||
|
};
|
||||||
|
fn b() *native.Backend {
|
||||||
|
return native.instance.?;
|
||||||
|
}
|
||||||
|
pub fn create(cmd: *const n.z_command, result: *n.z_request_result) u32 {
|
||||||
|
const root: ?*n.uhdk_file = @ptrCast(cmd.target);
|
||||||
|
const path = b().app.gpa.dupeZ(u8, abi.bytes(n.z_file_path(root))) catch return abi.OOM;
|
||||||
|
defer b().app.gpa.free(path);
|
||||||
|
const kind = c.z_linux_is_directory(path);
|
||||||
|
if (kind < 0 and c.z_linux_errno() != c.ENOENT) return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
const directory = b().app.gpa.dupeZ(u8, if (kind == 1) path else std.fs.path.dirname(path) orelse "/") catch return abi.OOM;
|
||||||
|
defer b().app.gpa.free(directory);
|
||||||
|
const fd = c.inotify_init1(c.IN_NONBLOCK | c.IN_CLOEXEC);
|
||||||
|
if (fd < 0) return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
const mask = c.IN_CREATE | c.IN_DELETE | c.IN_MOVED_FROM | c.IN_MOVED_TO | c.IN_MODIFY | c.IN_ATTRIB | c.IN_DELETE_SELF | c.IN_MOVE_SELF | c.IN_ONLYDIR;
|
||||||
|
if (c.inotify_add_watch(fd, directory, mask) < 0) {
|
||||||
|
_ = c.close(fd);
|
||||||
|
return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
}
|
||||||
|
const w = allocate(fd, root, @ptrCast(cmd.other), directory, if (kind == 1) "" else std.fs.path.basename(path)) catch {
|
||||||
|
_ = c.close(fd);
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
w.next = b().watches;
|
||||||
|
b().watches = w;
|
||||||
|
n.z_object_set_native(cmd.other, w);
|
||||||
|
result.value.watch = w.handle;
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
|
fn allocate(fd: c_int, root: ?*n.uhdk_file, handle: ?*n.uhdk_watch, directory: []const u8, name: []const u8) !*Watch {
|
||||||
|
const gpa = b().app.gpa;
|
||||||
|
const w = try gpa.create(Watch);
|
||||||
|
errdefer gpa.destroy(w);
|
||||||
|
const dir = try gpa.dupe(u8, directory);
|
||||||
|
errdefer gpa.free(dir);
|
||||||
|
w.* = .{ .fd = fd, .root = root, .handle = handle, .directory = dir, .name = try gpa.dupe(u8, name) };
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
pub fn poll(fds: *std.ArrayList(c.struct_pollfd)) !void {
|
||||||
|
var w = b().watches;
|
||||||
|
while (w) |item| : (w = item.next) if (item.fd >= 0) try fds.append(b().app.gpa, .{ .fd = item.fd, .events = c.POLLIN, .revents = 0 });
|
||||||
|
}
|
||||||
|
pub fn flags(mask: u32) u32 {
|
||||||
|
var value: u32 = 0;
|
||||||
|
if (mask & (c.IN_CREATE | c.IN_MOVED_TO) != 0) value |= n.UHDK_CHANGE_CREATED;
|
||||||
|
if (mask & (c.IN_DELETE | c.IN_MOVED_FROM) != 0) value |= n.UHDK_CHANGE_REMOVED;
|
||||||
|
if (mask & c.IN_MODIFY != 0) value |= n.UHDK_CHANGE_CONTENT;
|
||||||
|
if (mask & c.IN_ATTRIB != 0) value |= n.UHDK_CHANGE_METADATA;
|
||||||
|
if (mask & (c.IN_MOVED_TO | c.IN_MOVED_FROM) != 0) value |= n.UHDK_CHANGE_RENAMED;
|
||||||
|
if (mask & c.IN_Q_OVERFLOW != 0) value |= n.UHDK_CHANGE_RESCAN_REQUIRED;
|
||||||
|
if (mask & (c.IN_DELETE_SELF | c.IN_MOVE_SELF | c.IN_IGNORED | c.IN_UNMOUNT) != 0) value |= n.UHDK_CHANGE_ROOT_INVALIDATED;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
fn emit(w: *Watch, changes: u32, name: []const u8, status: u32) void {
|
||||||
|
var file = w.root;
|
||||||
|
if (w.name.len == 0 and name.len != 0) {
|
||||||
|
const path = std.fs.path.join(b().app.gpa, &.{ w.directory, name }) catch null;
|
||||||
|
if (path) |p| {
|
||||||
|
defer b().app.gpa.free(p);
|
||||||
|
file = n.z_file_from_url(runtime.toAppHandle(b().app), .{ .ptr = p.ptr, .len = p.len }, null);
|
||||||
|
} else file = null;
|
||||||
|
}
|
||||||
|
defer if (file != null and file != w.root) n.uhdk_file_release(file);
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_WATCH;
|
||||||
|
event.data.watch = .{ .watch = w.handle, .flags = changes, .status = status, .file = file };
|
||||||
|
n.z_emit(runtime.toAppHandle(b().app), &event);
|
||||||
|
}
|
||||||
|
pub fn tick() void {
|
||||||
|
var cursor = b().watches;
|
||||||
|
while (cursor) |w| : (cursor = w.next) {
|
||||||
|
if (w.fd < 0) continue;
|
||||||
|
var buffer: [65536]u8 align(@alignOf(c.struct_inotify_event)) = undefined;
|
||||||
|
const count = c.z_linux_read(w.fd, &buffer, buffer.len);
|
||||||
|
if (count < 0) {
|
||||||
|
if (c.z_linux_errno() != c.EAGAIN and c.z_linux_errno() != c.EINTR) {
|
||||||
|
emit(w, n.UHDK_CHANGE_ROOT_INVALIDATED, "", n.UHDK_STATUS_OS_ERROR);
|
||||||
|
_ = c.close(w.fd);
|
||||||
|
w.fd = -1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var offset: usize = 0;
|
||||||
|
while (offset + @sizeOf(c.struct_inotify_event) <= @as(usize, @intCast(count))) {
|
||||||
|
const event: *const c.struct_inotify_event = @ptrCast(@alignCast(&buffer[offset]));
|
||||||
|
const start = offset + @sizeOf(c.struct_inotify_event);
|
||||||
|
offset = start + event.len;
|
||||||
|
if (offset > @as(usize, @intCast(count))) break;
|
||||||
|
const bytes = buffer[start..offset];
|
||||||
|
const name = bytes[0 .. std.mem.findScalar(u8, bytes, 0) orelse bytes.len];
|
||||||
|
const changes = flags(event.mask);
|
||||||
|
const root_event = changes & (n.UHDK_CHANGE_ROOT_INVALIDATED | n.UHDK_CHANGE_RESCAN_REQUIRED) != 0;
|
||||||
|
if (changes != 0 and (root_event or w.name.len == 0 or std.mem.eql(u8, name, w.name))) emit(w, changes, if (root_event) "" else name, abi.OK);
|
||||||
|
if (changes & n.UHDK_CHANGE_ROOT_INVALIDATED != 0) {
|
||||||
|
_ = c.close(w.fd);
|
||||||
|
w.fd = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn destroy(w: *Watch) void {
|
||||||
|
var link = &b().watches;
|
||||||
|
while (link.* != w) link = &link.*.?.next;
|
||||||
|
link.* = w.next;
|
||||||
|
if (w.fd >= 0) _ = c.close(w.fd);
|
||||||
|
b().app.gpa.free(w.directory);
|
||||||
|
b().app.gpa.free(w.name);
|
||||||
|
b().app.gpa.destroy(w);
|
||||||
|
}
|
||||||
|
test "watch replacement is remove/create, overflow and invalidation stay distinct" {
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_CHANGE_CREATED | n.UHDK_CHANGE_RENAMED), flags(c.IN_MOVED_TO));
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_CHANGE_RESCAN_REQUIRED), flags(c.IN_Q_OVERFLOW));
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_CHANGE_ROOT_INVALIDATED), flags(c.IN_IGNORED));
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,12 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <sys/eventfd.h>
|
#include <sys/eventfd.h>
|
||||||
|
#include <sys/inotify.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include "wayland-client-core.h"
|
#include "wayland-client-core.h"
|
||||||
#include "wayland-egl-core.h"
|
#include "wayland-egl-core.h"
|
||||||
#define EGL_NO_X11
|
#define EGL_NO_X11
|
||||||
|
|
@ -16,6 +20,11 @@
|
||||||
/* Runtime imports are checked against upstream declarations in libraries.c.
|
/* Runtime imports are checked against upstream declarations in libraries.c.
|
||||||
* typeof(function) in translated Zig debug info can create unwanted link-time
|
* typeof(function) in translated Zig debug info can create unwanted link-time
|
||||||
* references, so the translated header uses explicit function-pointer types. */
|
* references, so the translated header uses explicit function-pointer types. */
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
#include <xkbcommon/xkbcommon-compose.h>
|
||||||
|
#include "xkb-imports.h"
|
||||||
|
#include <dbus/dbus.h>
|
||||||
|
#include "dbus-imports.h"
|
||||||
#include "function-pointers.h"
|
#include "function-pointers.h"
|
||||||
#include "redirects.h"
|
#include "redirects.h"
|
||||||
#include "wayland-client-protocol.h"
|
#include "wayland-client-protocol.h"
|
||||||
|
|
@ -23,12 +32,23 @@
|
||||||
#include "xdg-decoration-unstable-v1.h"
|
#include "xdg-decoration-unstable-v1.h"
|
||||||
#include "viewporter.h"
|
#include "viewporter.h"
|
||||||
#include "fractional-scale-v1.h"
|
#include "fractional-scale-v1.h"
|
||||||
|
#include "cursor-shape-v1.h"
|
||||||
|
#include "text-input-unstable-v3.h"
|
||||||
|
#include "xdg-activation-v1.h"
|
||||||
|
#include "xdg-foreign-unstable-v2.h"
|
||||||
|
#include "xdg-toplevel-icon-v1.h"
|
||||||
int z_linux_libraries_open(void);
|
int z_linux_libraries_open(void);
|
||||||
int z_linux_egl_available(void);
|
int z_linux_egl_available(void);
|
||||||
|
int z_linux_xkb_available(void);
|
||||||
|
int z_linux_dbus_available(void);
|
||||||
|
int z_linux_is_directory(const char *);
|
||||||
|
int z_linux_nonblock(int);
|
||||||
|
int z_linux_pipe(int[2]);
|
||||||
void z_linux_libraries_close(void);
|
void z_linux_libraries_close(void);
|
||||||
int z_linux_errno(void);
|
int z_linux_errno(void);
|
||||||
int z_linux_poll(struct pollfd *, nfds_t, int);
|
int z_linux_poll(struct pollfd *, nfds_t, int);
|
||||||
ssize_t z_linux_read(int, void *, size_t);
|
ssize_t z_linux_read(int, void *, size_t);
|
||||||
ssize_t z_linux_write(int, const void *, size_t);
|
ssize_t z_linux_write(int, const void *, size_t);
|
||||||
|
ssize_t z_linux_pipe_write(int, const void *, size_t);
|
||||||
int z_linux_is_main_thread(void);
|
int z_linux_is_main_thread(void);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ pub fn configure(b: *std.Build, module: *std.Build.Module, target: std.Build.Res
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
for ([_][]const u8{ "src/linux", "src/linux/vendor/wayland", "src/linux/vendor/khronos", "src/linux/protocols/generated" }) |path| {
|
for ([_][]const u8{ "src/linux", "src/linux/vendor/wayland", "src/linux/vendor", "src/linux/vendor/dbus", "src/linux/vendor/khronos", "src/linux/protocols/generated" }) |path| {
|
||||||
bindings.addIncludePath(b.path(path));
|
bindings.addIncludePath(b.path(path));
|
||||||
module.addIncludePath(b.path(path));
|
module.addIncludePath(b.path(path));
|
||||||
}
|
}
|
||||||
module.addImport("linux_c", bindings.createModule());
|
module.addImport("linux_c", bindings.createModule());
|
||||||
module.addCSourceFile(.{ .file = b.path("src/linux/libraries.c"), .flags = &.{ "-std=gnu11", "-Wall", "-Wextra", "-Werror", "-fvisibility=hidden" } });
|
module.addCSourceFile(.{ .file = b.path("src/linux/libraries.c"), .flags = &.{ "-std=gnu11", "-Wall", "-Wextra", "-Werror", "-fvisibility=hidden" } });
|
||||||
for ([_][]const u8{ "wayland", "xdg-shell", "xdg-decoration-unstable-v1", "viewporter", "fractional-scale-v1" }) |name| {
|
for ([_][]const u8{ "wayland", "xdg-shell", "xdg-decoration-unstable-v1", "viewporter", "fractional-scale-v1", "cursor-shape-v1", "tablet-v2", "text-input-unstable-v3", "xdg-activation-v1", "xdg-foreign-unstable-v2", "xdg-toplevel-icon-v1" }) |name| {
|
||||||
module.addCSourceFile(.{
|
module.addCSourceFile(.{
|
||||||
.file = b.path(b.fmt("src/linux/protocols/generated/{s}.c", .{name})),
|
.file = b.path(b.fmt("src/linux/protocols/generated/{s}.c", .{name})),
|
||||||
.flags = &.{ "-std=c99", "-include", b.path("src/linux/protocols/generated/private-symbols.h").getPath(b) },
|
.flags = &.{ "-std=c99", "-include", b.path("src/linux/protocols/generated/private-symbols.h").getPath(b) },
|
||||||
|
|
@ -30,5 +30,18 @@ pub fn addTests(b: *std.Build, lib: *std.Build.Step.Compile, target: std.Build.R
|
||||||
check.dependOn(&exe.step);
|
check.dependOn(&exe.step);
|
||||||
b.step("test-linux-native", "Exercise Wayland/EGL lifecycle in the active desktop session").dependOn(&b.addRunArtifact(exe).step);
|
b.step("test-linux-native", "Exercise Wayland/EGL lifecycle in the active desktop session").dependOn(&b.addRunArtifact(exe).step);
|
||||||
const install = b.addInstallArtifact(exe, .{});
|
const install = b.addInstallArtifact(exe, .{});
|
||||||
b.step("linux-test-bin", "Install the Linux native test harness").dependOn(&install.step);
|
b.step("linux-test-bin", "Install the Linux native test harnesses").dependOn(&install.step);
|
||||||
|
addServiceTests(b, lib, target, optimize, check);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept separate from automatic native smoke: fixture tests supply an isolated
|
||||||
|
// compositor/session bus, and the live mode explicitly checks installed portals.
|
||||||
|
pub fn addServiceTests(b: *std.Build, lib: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, check: *std.Build.Step) void {
|
||||||
|
const module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true });
|
||||||
|
module.addIncludePath(b.path("include"));
|
||||||
|
module.addCSourceFile(.{ .file = b.path("src/linux/tests/services.c"), .flags = &.{ "-std=c99", "-Wall", "-Wextra", "-Werror" } });
|
||||||
|
module.linkLibrary(lib);
|
||||||
|
const exe = b.addExecutable(.{ .name = "linux-service-tests", .root_module = module });
|
||||||
|
check.dependOn(&exe.step);
|
||||||
|
b.top_level_steps.get("linux-test-bin").?.step.dependOn(&b.addInstallArtifact(exe, .{}).step);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/* Runtime ABI pointers, type-checked against upstream in libraries.c. */
|
||||||
|
#ifndef UHDK_DBUS_IMPORTS_H
|
||||||
|
#define UHDK_DBUS_IMPORTS_H
|
||||||
|
extern DBusConnection *(*z_dbus_connection_open_private)(const char *address, DBusError *error);
|
||||||
|
extern void (*z_dbus_connection_set_exit_on_disconnect)(
|
||||||
|
DBusConnection *connection,
|
||||||
|
dbus_bool_t exit_on_disconnect
|
||||||
|
);
|
||||||
|
extern void (*z_dbus_connection_close)(DBusConnection *connection);
|
||||||
|
extern void (*z_dbus_connection_unref)(DBusConnection *connection);
|
||||||
|
extern dbus_bool_t (*z_dbus_connection_get_is_connected)(DBusConnection *connection);
|
||||||
|
extern dbus_bool_t (*z_dbus_connection_get_unix_fd)(DBusConnection *connection, int *fd);
|
||||||
|
extern dbus_bool_t (*z_dbus_connection_has_messages_to_send)(DBusConnection *connection);
|
||||||
|
extern dbus_bool_t (*z_dbus_connection_read_write)(
|
||||||
|
DBusConnection *connection,
|
||||||
|
int timeout_milliseconds
|
||||||
|
);
|
||||||
|
extern DBusMessage *(*z_dbus_connection_pop_message)(DBusConnection *connection);
|
||||||
|
extern dbus_bool_t (*z_dbus_connection_send)(
|
||||||
|
DBusConnection *connection,
|
||||||
|
DBusMessage *message,
|
||||||
|
dbus_uint32_t *client_serial
|
||||||
|
);
|
||||||
|
extern DBusMessage *(*z_dbus_message_new_method_call)(
|
||||||
|
const char *bus_name,
|
||||||
|
const char *path,
|
||||||
|
const char *iface,
|
||||||
|
const char *method
|
||||||
|
);
|
||||||
|
extern void (*z_dbus_message_unref)(DBusMessage *message);
|
||||||
|
extern int (*z_dbus_message_get_type)(DBusMessage *message);
|
||||||
|
extern dbus_uint32_t (*z_dbus_message_get_reply_serial)(DBusMessage *message);
|
||||||
|
extern const char *(*z_dbus_message_get_path)(DBusMessage *message);
|
||||||
|
extern const char *(*z_dbus_message_get_sender)(DBusMessage *message);
|
||||||
|
extern const char *(*z_dbus_message_get_error_name)(DBusMessage *message);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_is_signal)(
|
||||||
|
DBusMessage *message,
|
||||||
|
const char *iface,
|
||||||
|
const char *signal_name
|
||||||
|
);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_has_signature)(DBusMessage *message, const char *signature);
|
||||||
|
extern void (*z_dbus_message_set_no_reply)(DBusMessage *message, dbus_bool_t no_reply);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_iter_init)(DBusMessage *message, DBusMessageIter *iter);
|
||||||
|
extern void (*z_dbus_message_iter_init_append)(DBusMessage *message, DBusMessageIter *iter);
|
||||||
|
extern int (*z_dbus_message_iter_get_arg_type)(DBusMessageIter *iter);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_iter_next)(DBusMessageIter *iter);
|
||||||
|
extern void (*z_dbus_message_iter_recurse)(DBusMessageIter *iter, DBusMessageIter *sub);
|
||||||
|
extern void (*z_dbus_message_iter_get_basic)(DBusMessageIter *iter, void *value);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_iter_append_basic)(
|
||||||
|
DBusMessageIter *iter,
|
||||||
|
int type,
|
||||||
|
const void *value
|
||||||
|
);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_iter_open_container)(
|
||||||
|
DBusMessageIter *iter,
|
||||||
|
int type,
|
||||||
|
const char *contained_signature,
|
||||||
|
DBusMessageIter *sub
|
||||||
|
);
|
||||||
|
extern dbus_bool_t (*z_dbus_message_iter_close_container)(
|
||||||
|
DBusMessageIter *iter,
|
||||||
|
DBusMessageIter *sub
|
||||||
|
);
|
||||||
|
extern void (*z_dbus_message_iter_abandon_container_if_open)(
|
||||||
|
DBusMessageIter *iter,
|
||||||
|
DBusMessageIter *sub
|
||||||
|
);
|
||||||
|
extern dbus_bool_t (*z_dbus_threads_init_default)(void);
|
||||||
|
#endif
|
||||||
|
|
@ -30,3 +30,54 @@ Z_FUNCTION(eglSwapBuffers)
|
||||||
Z_FUNCTION(eglSwapInterval)
|
Z_FUNCTION(eglSwapInterval)
|
||||||
Z_FUNCTION(eglGetProcAddress)
|
Z_FUNCTION(eglGetProcAddress)
|
||||||
Z_FUNCTION(eglReleaseThread)
|
Z_FUNCTION(eglReleaseThread)
|
||||||
|
Z_FUNCTION(xkb_context_new)
|
||||||
|
Z_FUNCTION(xkb_context_unref)
|
||||||
|
Z_FUNCTION(xkb_keymap_new_from_string)
|
||||||
|
Z_FUNCTION(xkb_keymap_unref)
|
||||||
|
Z_FUNCTION(xkb_keymap_key_repeats)
|
||||||
|
Z_FUNCTION(xkb_state_new)
|
||||||
|
Z_FUNCTION(xkb_state_unref)
|
||||||
|
Z_FUNCTION(xkb_state_update_mask)
|
||||||
|
Z_FUNCTION(xkb_state_key_get_one_sym)
|
||||||
|
Z_FUNCTION(xkb_state_key_get_utf8)
|
||||||
|
Z_FUNCTION(xkb_state_mod_name_is_active)
|
||||||
|
Z_FUNCTION(xkb_keysym_to_utf32)
|
||||||
|
Z_FUNCTION(xkb_compose_table_new_from_locale)
|
||||||
|
Z_FUNCTION(xkb_compose_table_unref)
|
||||||
|
Z_FUNCTION(xkb_compose_state_new)
|
||||||
|
Z_FUNCTION(xkb_compose_state_unref)
|
||||||
|
Z_FUNCTION(xkb_compose_state_feed)
|
||||||
|
Z_FUNCTION(xkb_compose_state_reset)
|
||||||
|
Z_FUNCTION(xkb_compose_state_get_status)
|
||||||
|
Z_FUNCTION(xkb_compose_state_get_utf8)
|
||||||
|
Z_FUNCTION(dbus_connection_open_private)
|
||||||
|
Z_FUNCTION(dbus_connection_set_exit_on_disconnect)
|
||||||
|
Z_FUNCTION(dbus_connection_close)
|
||||||
|
Z_FUNCTION(dbus_connection_unref)
|
||||||
|
Z_FUNCTION(dbus_connection_get_is_connected)
|
||||||
|
Z_FUNCTION(dbus_connection_get_unix_fd)
|
||||||
|
Z_FUNCTION(dbus_connection_has_messages_to_send)
|
||||||
|
Z_FUNCTION(dbus_connection_read_write)
|
||||||
|
Z_FUNCTION(dbus_connection_pop_message)
|
||||||
|
Z_FUNCTION(dbus_connection_send)
|
||||||
|
Z_FUNCTION(dbus_message_new_method_call)
|
||||||
|
Z_FUNCTION(dbus_message_unref)
|
||||||
|
Z_FUNCTION(dbus_message_get_type)
|
||||||
|
Z_FUNCTION(dbus_message_get_reply_serial)
|
||||||
|
Z_FUNCTION(dbus_message_get_path)
|
||||||
|
Z_FUNCTION(dbus_message_get_sender)
|
||||||
|
Z_FUNCTION(dbus_message_get_error_name)
|
||||||
|
Z_FUNCTION(dbus_message_is_signal)
|
||||||
|
Z_FUNCTION(dbus_message_has_signature)
|
||||||
|
Z_FUNCTION(dbus_message_set_no_reply)
|
||||||
|
Z_FUNCTION(dbus_message_iter_init)
|
||||||
|
Z_FUNCTION(dbus_message_iter_init_append)
|
||||||
|
Z_FUNCTION(dbus_message_iter_get_arg_type)
|
||||||
|
Z_FUNCTION(dbus_message_iter_next)
|
||||||
|
Z_FUNCTION(dbus_message_iter_recurse)
|
||||||
|
Z_FUNCTION(dbus_message_iter_get_basic)
|
||||||
|
Z_FUNCTION(dbus_message_iter_append_basic)
|
||||||
|
Z_FUNCTION(dbus_message_iter_open_container)
|
||||||
|
Z_FUNCTION(dbus_message_iter_close_container)
|
||||||
|
Z_FUNCTION(dbus_message_iter_abandon_container_if_open)
|
||||||
|
Z_FUNCTION(dbus_threads_init_default)
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,31 @@
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "wayland-client-core.h"
|
#include "wayland-client-core.h"
|
||||||
#include "wayland-egl-core.h"
|
#include "wayland-egl-core.h"
|
||||||
#define EGL_NO_X11
|
#define EGL_NO_X11
|
||||||
#include "EGL/egl.h"
|
#include "EGL/egl.h"
|
||||||
#include "EGL/eglext.h"
|
#include "EGL/eglext.h"
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
#include <xkbcommon/xkbcommon-compose.h>
|
||||||
|
#include "xkb-imports.h"
|
||||||
|
#include <dbus/dbus.h>
|
||||||
|
#include "dbus-imports.h"
|
||||||
#include "function-pointers.h"
|
#include "function-pointers.h"
|
||||||
#define Z_FUNCTION(name) __typeof__(name) *z_##name;
|
#define Z_FUNCTION(name) __typeof__(name) *z_##name;
|
||||||
#include "functions.inc"
|
#include "functions.inc"
|
||||||
#undef Z_FUNCTION
|
#undef Z_FUNCTION
|
||||||
|
|
||||||
static void *wayland, *wayland_egl, *egl;
|
static void *wayland, *wayland_egl, *egl, *xkb, *dbus;
|
||||||
static int have_egl;
|
static int have_egl, have_xkb, have_dbus;
|
||||||
|
|
||||||
static void *open_library(const char *name) {
|
static void *open_library(const char *name) {
|
||||||
void *handle = dlopen(name, RTLD_NOW | RTLD_LOCAL);
|
void *handle = dlopen(name, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
|
@ -36,7 +45,13 @@ static void *open_library(const char *name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void z_linux_libraries_close(void) {
|
void z_linux_libraries_close(void) {
|
||||||
have_egl = 0;
|
have_egl = have_xkb = have_dbus = 0;
|
||||||
|
if (dbus)
|
||||||
|
dlclose(dbus);
|
||||||
|
dbus = NULL;
|
||||||
|
if (xkb)
|
||||||
|
dlclose(xkb);
|
||||||
|
xkb = NULL;
|
||||||
if (egl)
|
if (egl)
|
||||||
dlclose(egl);
|
dlclose(egl);
|
||||||
if (wayland_egl)
|
if (wayland_egl)
|
||||||
|
|
@ -56,19 +71,34 @@ int z_linux_libraries_open(void) {
|
||||||
wayland_egl = open_library("libwayland-egl.so.1");
|
wayland_egl = open_library("libwayland-egl.so.1");
|
||||||
egl = open_library("libEGL.so.1");
|
egl = open_library("libEGL.so.1");
|
||||||
have_egl = wayland_egl && egl;
|
have_egl = wayland_egl && egl;
|
||||||
|
xkb = open_library("libxkbcommon.so.0");
|
||||||
|
have_xkb = xkb != NULL;
|
||||||
|
dbus = open_library("libdbus-1.so.3");
|
||||||
|
have_dbus = dbus != NULL;
|
||||||
#define Z_FUNCTION(name) \
|
#define Z_FUNCTION(name) \
|
||||||
do { \
|
do { \
|
||||||
const char *symbol = #name; \
|
const char *symbol = #name; \
|
||||||
|
int bus = strncmp(symbol, "dbus_", 5) == 0; \
|
||||||
|
int keyboard = strncmp(symbol, "xkb_", 4) == 0; \
|
||||||
int graphics = strncmp(symbol, "egl", 3) == 0 || strncmp(symbol, "wl_egl_", 7) == 0; \
|
int graphics = strncmp(symbol, "egl", 3) == 0 || strncmp(symbol, "wl_egl_", 7) == 0; \
|
||||||
void *library = strncmp(symbol, "egl", 3) == 0 ? egl : (graphics ? wayland_egl : wayland); \
|
void *library = \
|
||||||
|
bus ? dbus \
|
||||||
|
: keyboard \
|
||||||
|
? xkb \
|
||||||
|
: (strncmp(symbol, "egl", 3) == 0 ? egl : (graphics ? wayland_egl : wayland)); \
|
||||||
if (library) \
|
if (library) \
|
||||||
z_##name = (__typeof__(z_##name))dlsym(library, symbol); \
|
z_##name = (__typeof__(z_##name))dlsym(library, symbol); \
|
||||||
if (!z_##name) { \
|
if (!z_##name) { \
|
||||||
if (!graphics) { \
|
if (bus) \
|
||||||
|
have_dbus = 0; \
|
||||||
|
else if (keyboard) \
|
||||||
|
have_xkb = 0; \
|
||||||
|
else if (graphics) \
|
||||||
|
have_egl = 0; \
|
||||||
|
else { \
|
||||||
z_linux_libraries_close(); \
|
z_linux_libraries_close(); \
|
||||||
return 0; \
|
return 0; \
|
||||||
} \
|
} \
|
||||||
have_egl = 0; \
|
|
||||||
} \
|
} \
|
||||||
} while (0);
|
} while (0);
|
||||||
#include "functions.inc"
|
#include "functions.inc"
|
||||||
|
|
@ -96,3 +126,57 @@ ssize_t z_linux_read(int fd, void *buffer, size_t count) {
|
||||||
ssize_t z_linux_write(int fd, const void *buffer, size_t count) {
|
ssize_t z_linux_write(int fd, const void *buffer, size_t count) {
|
||||||
return write(fd, buffer, count);
|
return write(fd, buffer, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int z_linux_xkb_available(void) {
|
||||||
|
return have_xkb;
|
||||||
|
}
|
||||||
|
int z_linux_nonblock(int fd) {
|
||||||
|
int flags = fcntl(fd, F_GETFL);
|
||||||
|
return flags < 0 ? -1 : fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
}
|
||||||
|
int z_linux_pipe(int fds[2]) {
|
||||||
|
if (pipe2(fds, O_CLOEXEC) < 0)
|
||||||
|
return -1;
|
||||||
|
if (z_linux_nonblock(fds[0]) == 0)
|
||||||
|
return 0;
|
||||||
|
int saved = errno;
|
||||||
|
close(fds[0]);
|
||||||
|
close(fds[1]);
|
||||||
|
errno = saved;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A clipboard reader may close early. Suppress only the SIGPIPE generated by
|
||||||
|
* this write, preserving the embedding application's signal disposition. */
|
||||||
|
ssize_t z_linux_pipe_write(int fd, const void *buffer, size_t count) {
|
||||||
|
sigset_t set, old, pending;
|
||||||
|
sigemptyset(&set);
|
||||||
|
sigaddset(&set, SIGPIPE);
|
||||||
|
if (pthread_sigmask(SIG_BLOCK, &set, &old) != 0) {
|
||||||
|
errno = EIO;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sigpending(&pending);
|
||||||
|
int was_pending = sigismember(&pending, SIGPIPE);
|
||||||
|
ssize_t result = write(fd, buffer, count);
|
||||||
|
int saved = errno;
|
||||||
|
if (result < 0 && saved == EPIPE && !was_pending) {
|
||||||
|
struct timespec zero = {0, 0};
|
||||||
|
while (sigtimedwait(&set, NULL, &zero) < 0 && errno == EINTR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_sigmask(SIG_SETMASK, &old, NULL);
|
||||||
|
errno = saved;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int z_linux_is_directory(const char *path) {
|
||||||
|
struct stat info;
|
||||||
|
return stat(path, &info) < 0 ? -1 : S_ISDIR(info.st_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
int z_linux_dbus_available(void) {
|
||||||
|
return have_dbus;
|
||||||
|
}
|
||||||
|
_Static_assert(sizeof(dbus_uint32_t) == 4 && sizeof(dbus_uint64_t) == 8, "D-Bus integer ABI");
|
||||||
|
_Static_assert(sizeof(DBusMessageIter) == (sizeof(void *) == 8 ? 72 : 56), "D-Bus iterator ABI");
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ const abi = @import("../abi.zig");
|
||||||
const runtime = @import("../runtime.zig");
|
const runtime = @import("../runtime.zig");
|
||||||
const Application = @import("../Application.zig");
|
const Application = @import("../Application.zig");
|
||||||
const Egl = @import("Egl.zig");
|
const Egl = @import("Egl.zig");
|
||||||
|
const Input = @import("Input.zig");
|
||||||
|
const Desktop = @import("Desktop.zig");
|
||||||
|
const Portal = @import("Portal.zig");
|
||||||
|
const Watch = @import("Watch.zig");
|
||||||
|
const Data = @import("Data.zig");
|
||||||
|
const Text = @import("Text.zig");
|
||||||
|
|
||||||
pub const Window = struct {
|
pub const Window = struct {
|
||||||
next: ?*Window = null,
|
next: ?*Window = null,
|
||||||
|
|
@ -37,6 +43,9 @@ pub const Window = struct {
|
||||||
focused: bool = false,
|
focused: bool = false,
|
||||||
state: u32 = n.UHDK_WINDOW_NORMAL,
|
state: u32 = n.UHDK_WINDOW_NORMAL,
|
||||||
flags: u32,
|
flags: u32,
|
||||||
|
cursor: u32 = n.UHDK_CURSOR_ARROW,
|
||||||
|
text: Text.State = .{},
|
||||||
|
drop_enabled: bool = false,
|
||||||
metrics: n.uhdk_window_metrics = std.mem.zeroes(n.uhdk_window_metrics),
|
metrics: n.uhdk_window_metrics = std.mem.zeroes(n.uhdk_window_metrics),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -52,6 +61,12 @@ pub const Backend = struct {
|
||||||
windows: ?*Window = null,
|
windows: ?*Window = null,
|
||||||
egl_display: c.EGLDisplay = null,
|
egl_display: c.EGLDisplay = null,
|
||||||
egl_config: c.EGLConfig = null,
|
egl_config: c.EGLConfig = null,
|
||||||
|
input: Input.State = .{},
|
||||||
|
data: Data.State = .{},
|
||||||
|
watches: ?*Watch.Watch = null,
|
||||||
|
portal: Portal.State = .{},
|
||||||
|
desktop: Desktop.State = .{},
|
||||||
|
capabilities: std.atomic.Value(u32) = .init(0),
|
||||||
running: bool = true,
|
running: bool = true,
|
||||||
failed: std.atomic.Value(bool) = .init(false),
|
failed: std.atomic.Value(bool) = .init(false),
|
||||||
};
|
};
|
||||||
|
|
@ -67,7 +82,12 @@ pub fn capability(handle: ?*n.uhdk_app, cap: u32) n.uhdk_bool {
|
||||||
if (handle == null) return 0;
|
if (handle == null) return 0;
|
||||||
const b = instance orelse return 0;
|
const b = instance orelse return 0;
|
||||||
if (b.app != runtime.fromAppHandle(handle) or !b.app.running or b.failed.load(.acquire)) return 0;
|
if (b.app != runtime.fromAppHandle(handle) or !b.app.running or b.failed.load(.acquire)) return 0;
|
||||||
return @intFromBool(cap == n.UHDK_CAP_OPENGL and b.egl_display != null);
|
return @intFromBool(cap < 32 and b.capabilities.load(.acquire) & (@as(u32, 1) << @intCast(cap)) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setCapability(cap: u32, available: bool) void {
|
||||||
|
const bits = @as(u32, 1) << @intCast(cap);
|
||||||
|
if (available) _ = instance.?.capabilities.fetchOr(bits, .release) else _ = instance.?.capabilities.fetchAnd(~bits, .release);
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn z_native_is_main_thread() c_int {
|
export fn z_native_is_main_thread() c_int {
|
||||||
|
|
@ -82,6 +102,7 @@ export fn z_native_start(handle: ?*n.uhdk_app, _: n.uhdk_text) u32 {
|
||||||
};
|
};
|
||||||
b.* = .{ .app = app };
|
b.* = .{ .app = app };
|
||||||
instance = b;
|
instance = b;
|
||||||
|
Input.initialize();
|
||||||
const fd = c.eventfd(0, c.EFD_CLOEXEC | c.EFD_NONBLOCK);
|
const fd = c.eventfd(0, c.EFD_CLOEXEC | c.EFD_NONBLOCK);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
z_native_finish();
|
z_native_finish();
|
||||||
|
|
@ -106,7 +127,15 @@ export fn z_native_start(handle: ?*n.uhdk_app, _: n.uhdk_text) u32 {
|
||||||
z_native_finish();
|
z_native_finish();
|
||||||
return n.UHDK_STATUS_UNSUPPORTED;
|
return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
|
Input.setup();
|
||||||
|
if (!initialRegistry(b)) {
|
||||||
|
z_native_finish();
|
||||||
|
return n.UHDK_STATUS_OS_ERROR;
|
||||||
|
}
|
||||||
|
Desktop.initialize();
|
||||||
|
Portal.initialize();
|
||||||
Egl.initialize(b);
|
Egl.initialize(b);
|
||||||
|
setCapability(n.UHDK_CAP_OPENGL, b.egl_display != null);
|
||||||
return abi.OK;
|
return abi.OK;
|
||||||
}
|
}
|
||||||
// Startup also uses a bounded read/flush/poll handshake: an unresponsive
|
// Startup also uses a bounded read/flush/poll handshake: an unresponsive
|
||||||
|
|
@ -148,6 +177,11 @@ fn registryDone(data: ?*anyopaque, _: ?*c.struct_wl_callback, _: u32) callconv(.
|
||||||
export fn z_native_finish() void {
|
export fn z_native_finish() void {
|
||||||
const b = instance orelse return;
|
const b = instance orelse return;
|
||||||
std.debug.assert(b.windows == null);
|
std.debug.assert(b.windows == null);
|
||||||
|
while (b.watches) |w| Watch.destroy(w);
|
||||||
|
Desktop.deinit();
|
||||||
|
Portal.deinit();
|
||||||
|
Data.deinit();
|
||||||
|
Input.deinit();
|
||||||
Egl.finish(b);
|
Egl.finish(b);
|
||||||
if (b.fractional != null) c.wp_fractional_scale_manager_v1_destroy(b.fractional);
|
if (b.fractional != null) c.wp_fractional_scale_manager_v1_destroy(b.fractional);
|
||||||
if (b.viewporter != null) c.wp_viewporter_destroy(b.viewporter);
|
if (b.viewporter != null) c.wp_viewporter_destroy(b.viewporter);
|
||||||
|
|
@ -191,15 +225,24 @@ fn fail(b: *Backend) void {
|
||||||
}
|
}
|
||||||
export fn z_native_run() void {
|
export fn z_native_run() void {
|
||||||
const b = instance.?;
|
const b = instance.?;
|
||||||
|
var fds: std.ArrayList(c.struct_pollfd) = .empty;
|
||||||
|
defer fds.deinit(b.app.gpa);
|
||||||
while (b.running) {
|
while (b.running) {
|
||||||
if (!b.failed.load(.acquire) and c.z_wl_display_dispatch_pending.?(b.display) < 0) fail(b);
|
if (!b.failed.load(.acquire) and c.z_wl_display_dispatch_pending.?(b.display) < 0) fail(b);
|
||||||
n.z_tick(runtime.toAppHandle(b.app));
|
n.z_tick(runtime.toAppHandle(b.app));
|
||||||
var cursor = b.windows;
|
var cursor = b.windows;
|
||||||
var timeout: c_int = -1;
|
var timeout: c_int = Input.tick();
|
||||||
|
Watch.tick();
|
||||||
|
const desktop_timeout = Desktop.tick();
|
||||||
|
if (desktop_timeout >= 0) timeout = if (timeout < 0) desktop_timeout else @min(timeout, desktop_timeout);
|
||||||
|
const portal_timeout = Portal.tick();
|
||||||
|
if (portal_timeout >= 0) timeout = if (timeout < 0) portal_timeout else @min(timeout, portal_timeout);
|
||||||
|
const data_timeout = Data.tick();
|
||||||
|
if (data_timeout >= 0) timeout = if (timeout < 0) data_timeout else @min(timeout, data_timeout);
|
||||||
while (cursor) |w| {
|
while (cursor) |w| {
|
||||||
cursor = w.next;
|
cursor = w.next;
|
||||||
if (w.request != 0) {
|
if (w.request != 0) {
|
||||||
timeout = 50;
|
timeout = if (timeout < 0) 50 else @min(timeout, 50);
|
||||||
if (runtime.now(b.app.io) >= w.deadline) {
|
if (runtime.now(b.app.io) >= w.deadline) {
|
||||||
completeCreation(w, abi.UNAVAILABLE);
|
completeCreation(w, abi.UNAVAILABLE);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -221,21 +264,41 @@ export fn z_native_run() void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var fds = [_]c.struct_pollfd{
|
fds.clearRetainingCapacity();
|
||||||
|
fds.appendSlice(b.app.gpa, &.{
|
||||||
.{ .fd = if (reading) c.z_wl_display_get_fd.?(b.display) else -1, .events = display_events, .revents = 0 },
|
.{ .fd = if (reading) c.z_wl_display_get_fd.?(b.display) else -1, .events = display_events, .revents = 0 },
|
||||||
.{ .fd = wake_fd.load(.acquire), .events = c.POLLIN, .revents = 0 },
|
.{ .fd = wake_fd.load(.acquire), .events = c.POLLIN, .revents = 0 },
|
||||||
|
}) catch {
|
||||||
|
if (reading) c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
fail(b);
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
const ready = c.z_linux_poll(&fds, fds.len, timeout);
|
Data.poll(&fds) catch {
|
||||||
|
if (reading) c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
fail(b);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
Watch.poll(&fds) catch {
|
||||||
|
if (reading) c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
fail(b);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
Portal.poll(&fds) catch {
|
||||||
|
if (reading) c.z_wl_display_cancel_read.?(b.display);
|
||||||
|
fail(b);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const ready = c.z_linux_poll(fds.items.ptr, fds.items.len, timeout);
|
||||||
if (reading) {
|
if (reading) {
|
||||||
if (ready > 0 and fds[0].revents & c.POLLIN != 0) {
|
if (ready > 0 and fds.items[0].revents & c.POLLIN != 0) {
|
||||||
if (c.z_wl_display_read_events.?(b.display) < 0) fail(b);
|
if (c.z_wl_display_read_events.?(b.display) < 0) fail(b);
|
||||||
} else c.z_wl_display_cancel_read.?(b.display);
|
} else c.z_wl_display_cancel_read.?(b.display);
|
||||||
}
|
}
|
||||||
if (ready < 0 and c.z_linux_errno() != c.EINTR) fail(b);
|
if (ready < 0 and c.z_linux_errno() != c.EINTR) fail(b);
|
||||||
if (fds[0].revents & (c.POLLERR | c.POLLHUP | c.POLLNVAL) != 0) fail(b);
|
if (fds.items[0].revents & (c.POLLERR | c.POLLHUP | c.POLLNVAL) != 0) fail(b);
|
||||||
if (fds[1].revents & c.POLLIN != 0) {
|
if (fds.items[1].revents & c.POLLIN != 0) {
|
||||||
var value: u64 = 0;
|
var value: u64 = 0;
|
||||||
_ = c.z_linux_read(fds[1].fd, &value, @sizeOf(u64));
|
_ = c.z_linux_read(fds.items[1].fd, &value, @sizeOf(u64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -243,6 +306,10 @@ export fn z_native_run() void {
|
||||||
fn registryGlobal(data: ?*anyopaque, registry: ?*c.struct_wl_registry, name: u32, interface: [*c]const u8, version: u32) callconv(.c) void {
|
fn registryGlobal(data: ?*anyopaque, registry: ?*c.struct_wl_registry, name: u32, interface: [*c]const u8, version: u32) callconv(.c) void {
|
||||||
const b: *Backend = @ptrCast(@alignCast(data.?));
|
const b: *Backend = @ptrCast(@alignCast(data.?));
|
||||||
const str = std.mem.span(interface);
|
const str = std.mem.span(interface);
|
||||||
|
Input.global(registry, name, str, version);
|
||||||
|
Data.global(registry, name, str, version);
|
||||||
|
Portal.global(registry, name, str, version);
|
||||||
|
Desktop.global(registry, name, str, version);
|
||||||
if (std.mem.eql(u8, str, "wl_compositor") and version >= 4) {
|
if (std.mem.eql(u8, str, "wl_compositor") and version >= 4) {
|
||||||
b.compositor = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_compositor_interface, @min(version, 6)));
|
b.compositor = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wl_compositor_interface, @min(version, 6)));
|
||||||
} else if (std.mem.eql(u8, str, "xdg_wm_base")) {
|
} else if (std.mem.eql(u8, str, "xdg_wm_base")) {
|
||||||
|
|
@ -256,7 +323,9 @@ fn registryGlobal(data: ?*anyopaque, registry: ?*c.struct_wl_registry, name: u32
|
||||||
b.fractional = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_fractional_scale_manager_v1_interface, 1));
|
b.fractional = @ptrCast(c.wl_registry_bind(registry, name, &c.z_uhdk_wp_fractional_scale_manager_v1_interface, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn registryRemove(_: ?*anyopaque, _: ?*c.struct_wl_registry, _: u32) callconv(.c) void {}
|
fn registryRemove(_: ?*anyopaque, _: ?*c.struct_wl_registry, name: u32) callconv(.c) void {
|
||||||
|
Input.remove(name);
|
||||||
|
}
|
||||||
const registry_listener: c.struct_wl_registry_listener = .{ .global = registryGlobal, .global_remove = registryRemove };
|
const registry_listener: c.struct_wl_registry_listener = .{ .global = registryGlobal, .global_remove = registryRemove };
|
||||||
fn ping(_: ?*anyopaque, shell: ?*c.struct_xdg_wm_base, serial: u32) callconv(.c) void {
|
fn ping(_: ?*anyopaque, shell: ?*c.struct_xdg_wm_base, serial: u32) callconv(.c) void {
|
||||||
c.xdg_wm_base_pong(shell, serial);
|
c.xdg_wm_base_pong(shell, serial);
|
||||||
|
|
@ -411,6 +480,7 @@ fn setupWindow(b: *Backend, w: *Window, desc: n.uhdk_window_desc) u32 {
|
||||||
if (c.xdg_toplevel_add_listener(w.top, &top_listener, w) != 0) return abi.OOM;
|
if (c.xdg_toplevel_add_listener(w.top, &top_listener, w) != 0) return abi.OOM;
|
||||||
c.xdg_toplevel_set_title(w.top, title);
|
c.xdg_toplevel_set_title(w.top, title);
|
||||||
c.xdg_toplevel_set_app_id(w.top, app_id);
|
c.xdg_toplevel_set_app_id(w.top, app_id);
|
||||||
|
Desktop.applyIcon(w);
|
||||||
if (desc.parent) |parent| {
|
if (desc.parent) |parent| {
|
||||||
const p: *Window = @ptrCast(@alignCast(n.z_object_native(parent) orelse return abi.UNAVAILABLE));
|
const p: *Window = @ptrCast(@alignCast(n.z_object_native(parent) orelse return abi.UNAVAILABLE));
|
||||||
c.xdg_toplevel_set_parent(w.top, p.top);
|
c.xdg_toplevel_set_parent(w.top, p.top);
|
||||||
|
|
@ -445,6 +515,10 @@ fn setLimits(w: *Window) void {
|
||||||
fn destroyWindow(w: *Window) void {
|
fn destroyWindow(w: *Window) void {
|
||||||
const b = instance.?;
|
const b = instance.?;
|
||||||
std.debug.assert(w.egl == null);
|
std.debug.assert(w.egl == null);
|
||||||
|
Desktop.forget(w);
|
||||||
|
Data.forget(w);
|
||||||
|
Input.forget(w);
|
||||||
|
Text.deinit(w);
|
||||||
var link = &b.windows;
|
var link = &b.windows;
|
||||||
while (link.*) |item| {
|
while (link.*) |item| {
|
||||||
if (item == w) {
|
if (item == w) {
|
||||||
|
|
@ -473,6 +547,11 @@ export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64,
|
||||||
}
|
}
|
||||||
if (b.failed.load(.acquire)) return abi.UNAVAILABLE;
|
if (b.failed.load(.acquire)) return abi.UNAVAILABLE;
|
||||||
if (cmd.op == n.Z_WINDOW) return createWindow(b, cmd, id);
|
if (cmd.op == n.Z_WINDOW) return createWindow(b, cmd, id);
|
||||||
|
if (cmd.op == n.Z_ICON) return Desktop.setIcon(cmd.image);
|
||||||
|
if (cmd.op == n.Z_DIALOG or cmd.op == n.Z_URL) return Portal.request(cmd, id);
|
||||||
|
if (cmd.op == n.Z_WATCH) return Watch.create(cmd, &result.*);
|
||||||
|
if (cmd.op == n.Z_CLIP_WRITE) return Data.write(cmd.text);
|
||||||
|
if (cmd.op == n.Z_CLIP_READ) return Data.read(id, cmd.limit);
|
||||||
const ptr = if (cmd.target != null) n.z_object_native(cmd.target) else null;
|
const ptr = if (cmd.target != null) n.z_object_native(cmd.target) else null;
|
||||||
const w: *Window = if (ptr) |p| @ptrCast(@alignCast(p)) else return n.UHDK_STATUS_UNSUPPORTED;
|
const w: *Window = if (ptr) |p| @ptrCast(@alignCast(p)) else return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
switch (cmd.op) {
|
switch (cmd.op) {
|
||||||
|
|
@ -515,7 +594,13 @@ export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64,
|
||||||
if (w.want_visible and !w.configured) c.wl_surface_commit(w.surface);
|
if (w.want_visible and !w.configured) c.wl_surface_commit(w.surface);
|
||||||
w.dirty = true;
|
w.dirty = true;
|
||||||
},
|
},
|
||||||
n.Z_FOCUS => return n.UHDK_STATUS_UNSUPPORTED,
|
n.Z_DROP => return Data.enable(w, cmd.number != 0),
|
||||||
|
n.Z_CURSOR => return Input.cursor(w, @intCast(cmd.number)),
|
||||||
|
n.Z_POINTER => return Input.capture(w, cmd.number != 0),
|
||||||
|
n.Z_TEXT_ENABLE => return Text.enable(w, cmd.number != 0),
|
||||||
|
n.Z_TEXT_STATE => return Text.snapshot(w, cmd.text_state),
|
||||||
|
n.Z_TEXT_CANCEL => Text.cancel(w),
|
||||||
|
n.Z_FOCUS => return Desktop.focus(w, id),
|
||||||
n.Z_REDRAW => emit(w, n.UHDK_EVENT_WINDOW_REDRAW),
|
n.Z_REDRAW => emit(w, n.UHDK_EVENT_WINDOW_REDRAW),
|
||||||
n.Z_GL => return Egl.create(b, w, cmd.other, result),
|
n.Z_GL => return Egl.create(b, w, cmd.other, result),
|
||||||
else => return n.UHDK_STATUS_UNSUPPORTED,
|
else => return n.UHDK_STATUS_UNSUPPORTED,
|
||||||
|
|
@ -524,6 +609,7 @@ export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64,
|
||||||
}
|
}
|
||||||
export fn z_native_cancel(id: u64) void {
|
export fn z_native_cancel(id: u64) void {
|
||||||
const b = instance orelse return;
|
const b = instance orelse return;
|
||||||
|
if (Data.cancel(id) or Portal.cancel(id) or Desktop.cancel(id)) return;
|
||||||
var cursor = b.windows;
|
var cursor = b.windows;
|
||||||
while (cursor) |w| : (cursor = w.next) {
|
while (cursor) |w| : (cursor = w.next) {
|
||||||
if (w.request == id) {
|
if (w.request == id) {
|
||||||
|
|
@ -539,6 +625,7 @@ export fn z_native_destroy(kind: u32, ptr: ?*anyopaque) void {
|
||||||
const w: *Window = @ptrCast(@alignCast(p));
|
const w: *Window = @ptrCast(@alignCast(p));
|
||||||
if (w.request != 0) completeCreation(w, abi.CANCELLED) else destroyWindow(w);
|
if (w.request != 0) completeCreation(w, abi.CANCELLED) else destroyWindow(w);
|
||||||
},
|
},
|
||||||
|
n.Z_KIND_WATCH => Watch.destroy(@ptrCast(@alignCast(p))),
|
||||||
n.Z_KIND_GL => Egl.destroy(@ptrCast(@alignCast(p))),
|
n.Z_KIND_GL => Egl.destroy(@ptrCast(@alignCast(p))),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Exercise portal races on a private session bus and private Wayland socket.
|
||||||
|
|
||||||
|
Needs dbus-daemon and Python dbus-next (provided by the optional shell.nix).
|
||||||
|
Never calls the user's portal, opens a real dialog, or launches a URL handler.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from dbus_next import Message, MessageType, Variant
|
||||||
|
from dbus_next.aio import MessageBus
|
||||||
|
from wayland_fixture import run
|
||||||
|
|
||||||
|
DESKTOP = 'org.freedesktop.portal.Desktop'
|
||||||
|
PATH = '/org/freedesktop/portal/desktop'
|
||||||
|
REQUEST = 'org.freedesktop.portal.Request'
|
||||||
|
|
||||||
|
|
||||||
|
async def main(binary):
|
||||||
|
daemon = subprocess.Popen(['dbus-daemon', '--session', '--nofork', '--print-address=1'], stdout=subprocess.PIPE, text=True)
|
||||||
|
bus = None
|
||||||
|
tasks = set()
|
||||||
|
closed = []
|
||||||
|
seen = set()
|
||||||
|
try:
|
||||||
|
address = daemon.stdout.readline().strip()
|
||||||
|
assert address
|
||||||
|
bus = await MessageBus(bus_address=address).connect()
|
||||||
|
await bus.request_name(DESKTOP)
|
||||||
|
|
||||||
|
def response(handle, code=0, results=None):
|
||||||
|
bus.send(Message.new_signal(handle, REQUEST, 'Response', 'ua{sv}', [code, results or {}]))
|
||||||
|
|
||||||
|
async def delayed_reply(message, handle, delay, is_url=False):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
bus.send(Message.new_method_return(message, 'o', [handle]))
|
||||||
|
if is_url: response(handle)
|
||||||
|
|
||||||
|
async def restart():
|
||||||
|
await bus.release_name(DESKTOP)
|
||||||
|
await asyncio.sleep(.1)
|
||||||
|
await bus.request_name(DESKTOP)
|
||||||
|
|
||||||
|
def schedule(coro):
|
||||||
|
task = asyncio.create_task(coro)
|
||||||
|
tasks.add(task)
|
||||||
|
task.add_done_callback(tasks.discard)
|
||||||
|
|
||||||
|
def handle(message):
|
||||||
|
if message.message_type != MessageType.METHOD_CALL:
|
||||||
|
return False
|
||||||
|
if message.interface == 'org.freedesktop.DBus.Properties' and message.member == 'Get':
|
||||||
|
assert message.body == ['org.freedesktop.portal.FileChooser', 'version']
|
||||||
|
bus.send(Message.new_method_return(message, 'v', [Variant('u', 4)]))
|
||||||
|
elif message.interface == 'org.freedesktop.portal.Settings' and message.member == 'ReadAll':
|
||||||
|
assert message.body == [['org.freedesktop.appearance']]
|
||||||
|
bus.send(Message.new_method_return(message, 'a{sa{sv}}', [{'org.freedesktop.appearance': {
|
||||||
|
'color-scheme': Variant('u', 1), 'contrast': Variant('u', 1), 'reduced-motion': Variant('u', 1)}}]))
|
||||||
|
elif message.interface in ('org.freedesktop.portal.FileChooser', 'org.freedesktop.portal.OpenURI'):
|
||||||
|
parent, title, options = message.body
|
||||||
|
token = options['handle_token'].value
|
||||||
|
handle_path = PATH + '/request/' + message.sender[1:].replace('.', '_') + '/' + token
|
||||||
|
if message.member == 'OpenURI':
|
||||||
|
assert title == 'https://example.org/test'
|
||||||
|
schedule(delayed_reply(message, handle_path, .35, True))
|
||||||
|
seen.add('url')
|
||||||
|
return True
|
||||||
|
seen.add(title)
|
||||||
|
assert message.signature == 'ssa{sv}'
|
||||||
|
assert options['multiple'].value and not options['directory'].value
|
||||||
|
assert options['filters'].value == [['Text', [[0, '*.txt'], [0, '*.md']]]]
|
||||||
|
if title == 'success':
|
||||||
|
assert parent == 'wayland:fixture-parent'
|
||||||
|
assert options['modal'].value
|
||||||
|
# A fast response can beat the method return. The frontend
|
||||||
|
# must already be subscribed using its chosen handle token.
|
||||||
|
response(handle_path, results={'uris': Variant('as', ['file:///tmp/a%20b%25%FF.txt', 'file:///tmp/second'])})
|
||||||
|
bus.send(Message.new_method_return(message, 'o', [handle_path]))
|
||||||
|
elif title == 'cancel':
|
||||||
|
# Return a different handle after local cancellation; both
|
||||||
|
# the predicted and returned paths need Close calls.
|
||||||
|
schedule(delayed_reply(message, handle_path + '_late', .2))
|
||||||
|
elif title == 'parent':
|
||||||
|
bus.send(Message.new_method_return(message, 'o', [handle_path]))
|
||||||
|
elif title == 'restart':
|
||||||
|
schedule(restart())
|
||||||
|
elif title == 'dismiss':
|
||||||
|
bus.send(Message.new_method_return(message, 'o', [handle_path]))
|
||||||
|
response(handle_path, 1)
|
||||||
|
else:
|
||||||
|
raise AssertionError(title)
|
||||||
|
elif message.interface == REQUEST and message.member == 'Close':
|
||||||
|
closed.append(message.path)
|
||||||
|
bus.send(Message.new_method_return(message))
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
bus.add_message_handler(handle)
|
||||||
|
await asyncio.to_thread(run, binary, 'portal', dict(os.environ, DBUS_SESSION_BUS_ADDRESS=address))
|
||||||
|
await asyncio.sleep(.1)
|
||||||
|
assert {'success', 'cancel', 'url', 'restart', 'dismiss'} <= seen, seen
|
||||||
|
assert any(p.endswith('_late') for p in closed), closed
|
||||||
|
assert len(closed) >= 2, closed
|
||||||
|
print('private portal protocol/cancellation/restart fixture: PASS')
|
||||||
|
finally:
|
||||||
|
for task in tasks: task.cancel()
|
||||||
|
if bus: bus.disconnect()
|
||||||
|
daemon.terminate()
|
||||||
|
daemon.wait(timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main(str(Path(sys.argv[1]).resolve())))
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include "uhdk.h"
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define OK(expr) \
|
||||||
|
do { \
|
||||||
|
uhdk_status status_ = (expr); \
|
||||||
|
if (status_ != UHDK_STATUS_OK) { \
|
||||||
|
fprintf(stderr, "%d: %s: status %u\n", __LINE__, #expr, status_); \
|
||||||
|
abort(); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
static uhdk_text text(const char *s) {
|
||||||
|
return (uhdk_text){s, strlen(s)};
|
||||||
|
}
|
||||||
|
static uint64_t deadline(uhdk_app *app) {
|
||||||
|
return uhdk_app_get_time_ns(app) + 8000000000ull;
|
||||||
|
}
|
||||||
|
static uhdk_request_result wait_result(uhdk_app *app, uint64_t id) {
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (uhdk_app_get_time_ns(app) < end) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 50000000, &events));
|
||||||
|
for (uint64_t i = 0; i < events.len; ++i)
|
||||||
|
if (events.ptr[i].type == UHDK_EVENT_REQUEST_DONE) {
|
||||||
|
const uhdk_request_result *r = events.ptr[i].data;
|
||||||
|
if (r->id == id)
|
||||||
|
return *r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "request %lu timed out\n", (unsigned long)id);
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
static uhdk_window *window(uhdk_app *app) {
|
||||||
|
uhdk_window_desc desc = {0};
|
||||||
|
desc.struct_size = sizeof(desc);
|
||||||
|
desc.title = text("UHDK service tests");
|
||||||
|
desc.content_size = (uhdk_size){320, 240};
|
||||||
|
desc.flags = UHDK_WINDOW_DECORATED | UHDK_WINDOW_RESIZABLE;
|
||||||
|
uint64_t id;
|
||||||
|
OK(uhdk_window_create_async(app, &desc, &id));
|
||||||
|
uhdk_request_result r = wait_result(app, id);
|
||||||
|
OK(r.status);
|
||||||
|
return ((const uhdk_window_result *)r.data)->window;
|
||||||
|
}
|
||||||
|
static void wait_capability(uhdk_app *app, uint32_t cap) {
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (!uhdk_app_has_capability(app, cap) && uhdk_app_get_time_ns(app) < end) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 50000000, &events));
|
||||||
|
}
|
||||||
|
assert(uhdk_app_has_capability(app, cap));
|
||||||
|
}
|
||||||
|
static void write_file(const char *path, const char *bytes) {
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
assert(f);
|
||||||
|
assert(fwrite(bytes, 1, strlen(bytes), f) == strlen(bytes));
|
||||||
|
assert(fclose(f) == 0);
|
||||||
|
}
|
||||||
|
static void wait_change(uhdk_app *app, uhdk_watch *watch, uint32_t flag) {
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (uhdk_app_get_time_ns(app) < end) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 50000000, &events));
|
||||||
|
for (uint64_t i = 0; i < events.len; ++i)
|
||||||
|
if (events.ptr[i].type == UHDK_EVENT_WATCH) {
|
||||||
|
const uhdk_watch_event *e = events.ptr[i].data;
|
||||||
|
if (e->watch == watch && (e->flags & flag)) {
|
||||||
|
OK(e->status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "watch flag %x timed out\n", flag);
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
static void watches(uhdk_app *app) {
|
||||||
|
char directory[] = "/tmp/uhdk-watch-XXXXXX";
|
||||||
|
assert(mkdtemp(directory));
|
||||||
|
char path[256], replacement[256];
|
||||||
|
snprintf(path, sizeof(path), "%s/document", directory);
|
||||||
|
snprintf(replacement, sizeof(replacement), "%s/replacement", directory);
|
||||||
|
write_file(path, "old");
|
||||||
|
uhdk_file *file;
|
||||||
|
OK(uhdk_file_create_from_native_path(
|
||||||
|
app, (uhdk_bytes){(const uint8_t *)path, strlen(path)}, &file
|
||||||
|
));
|
||||||
|
uint64_t id;
|
||||||
|
OK(uhdk_watch_create_async(file, &id));
|
||||||
|
uhdk_request_result result = wait_result(app, id);
|
||||||
|
OK(result.status);
|
||||||
|
uhdk_watch *watch = ((const uhdk_watch_result *)result.data)->watch;
|
||||||
|
write_file(replacement, "new");
|
||||||
|
assert(rename(replacement, path) == 0);
|
||||||
|
wait_change(app, watch, UHDK_CHANGE_CREATED);
|
||||||
|
write_file(path, "after replacement");
|
||||||
|
wait_change(app, watch, UHDK_CHANGE_CONTENT);
|
||||||
|
assert(unlink(path) == 0);
|
||||||
|
wait_change(app, watch, UHDK_CHANGE_REMOVED);
|
||||||
|
write_file(path, "recreated");
|
||||||
|
wait_change(app, watch, UHDK_CHANGE_CREATED);
|
||||||
|
assert(unlink(path) == 0);
|
||||||
|
assert(rmdir(directory) == 0);
|
||||||
|
wait_change(app, watch, UHDK_CHANGE_ROOT_INVALIDATED);
|
||||||
|
uhdk_watch_destroy(watch);
|
||||||
|
uhdk_file_release(file);
|
||||||
|
puts("watch replacement/removal/recreation/invalidation: PASS");
|
||||||
|
}
|
||||||
|
static void real_desktop(uhdk_app *app) {
|
||||||
|
wait_capability(app, UHDK_CAP_FILE_DIALOGS);
|
||||||
|
uhdk_preferences prefs = {0};
|
||||||
|
prefs.struct_size = sizeof(prefs);
|
||||||
|
OK(uhdk_app_get_preferences(app, &prefs));
|
||||||
|
printf(
|
||||||
|
"Portal available; appearance=%u, reduced_motion=%d, high_contrast=%d\n",
|
||||||
|
prefs.appearance,
|
||||||
|
prefs.reduced_motion,
|
||||||
|
prefs.high_contrast
|
||||||
|
);
|
||||||
|
watches(app);
|
||||||
|
uhdk_window *w = window(app);
|
||||||
|
uint8_t rgba[] = {255, 0, 0, 128, 0, 255, 0, 255};
|
||||||
|
uhdk_image image = {0};
|
||||||
|
image.struct_size = sizeof(image);
|
||||||
|
image.width = 2;
|
||||||
|
image.height = 1;
|
||||||
|
image.stride_bytes = 8;
|
||||||
|
image.pixels = (uhdk_bytes){rgba, sizeof(rgba)};
|
||||||
|
uint64_t id;
|
||||||
|
OK(uhdk_app_set_icon_async(app, &image, &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
// A hidden, unfocused window must not commandeer a later user's keystroke.
|
||||||
|
OK(uhdk_clipboard_write_text_async(app, text("must not replace clipboard"), &id));
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_UNAVAILABLE);
|
||||||
|
uhdk_window_destroy(w);
|
||||||
|
}
|
||||||
|
static void portals(uhdk_app *app) {
|
||||||
|
wait_capability(app, UHDK_CAP_FILE_DIALOGS);
|
||||||
|
uhdk_window *w = window(app);
|
||||||
|
uhdk_file_dialog_desc desc = {0};
|
||||||
|
desc.struct_size = sizeof(desc);
|
||||||
|
desc.parent = w;
|
||||||
|
desc.title = text("success");
|
||||||
|
desc.kind = UHDK_DIALOG_OPEN_FILES;
|
||||||
|
uhdk_text extensions[] = {text("txt"), text("md")};
|
||||||
|
uhdk_file_filter filter = {text("Text"), {extensions, 2}};
|
||||||
|
desc.filters = &filter;
|
||||||
|
desc.filters_len = 1;
|
||||||
|
uint64_t id;
|
||||||
|
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||||
|
uhdk_request_result r = wait_result(app, id);
|
||||||
|
OK(r.status);
|
||||||
|
const uhdk_file_list *files = r.data;
|
||||||
|
assert(files->len == 2);
|
||||||
|
uint8_t bytes[256];
|
||||||
|
uint64_t len;
|
||||||
|
OK(uhdk_file_copy_representation(
|
||||||
|
files->ptr[0], UHDK_FILE_NATIVE_PATH, bytes, sizeof(bytes), &len
|
||||||
|
));
|
||||||
|
assert(len == 14 && memcmp(bytes, "/tmp/a b%\xff.txt", 14) == 0);
|
||||||
|
desc.title = text("cancel");
|
||||||
|
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||||
|
// Allow method submission, then cancel while the mock withholds its reply.
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 100000000, &events));
|
||||||
|
OK(uhdk_request_cancel(app, id));
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
|
||||||
|
desc.title = text("parent");
|
||||||
|
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||||
|
uhdk_window_destroy(w);
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
|
||||||
|
OK(uhdk_shell_open_url_async(app, text("https://example.org/test"), &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
desc.parent = NULL;
|
||||||
|
desc.title = text("restart");
|
||||||
|
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_UNAVAILABLE);
|
||||||
|
wait_capability(app, UHDK_CAP_FILE_DIALOGS);
|
||||||
|
desc.title = text("dismiss");
|
||||||
|
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
|
||||||
|
puts("portal results/cancellation/parent/restart: PASS");
|
||||||
|
}
|
||||||
|
static void input(uhdk_app *app) {
|
||||||
|
uhdk_window *w = window(app);
|
||||||
|
uint64_t id;
|
||||||
|
OK(uhdk_window_set_text_input_async(w, 1, &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
OK(uhdk_window_set_drop_enabled_async(w, 1, &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
uhdk_text_state state = {0};
|
||||||
|
state.struct_size = sizeof(state);
|
||||||
|
state.surrounding_text = text("abc");
|
||||||
|
state.revision = 77;
|
||||||
|
state.selection_start = 1;
|
||||||
|
state.selection_len = 1;
|
||||||
|
OK(uhdk_window_set_text_state(w, &state));
|
||||||
|
unsigned key = 0, repeat = 0, commit = 0, preedit = 0, pointer = 0, scroll = 0, dropped = 0,
|
||||||
|
copy = 0, compose = 0;
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (uhdk_app_get_time_ns(app) < end && !(key && repeat && commit >= 2 && preedit &&
|
||||||
|
pointer && scroll && dropped && copy && compose)) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 50000000, &events));
|
||||||
|
for (uint64_t i = 0; i < events.len; ++i) {
|
||||||
|
const uhdk_event *e = &events.ptr[i];
|
||||||
|
switch (e->type) {
|
||||||
|
case UHDK_EVENT_KEY: {
|
||||||
|
const uhdk_key_event *k = e->data;
|
||||||
|
if (k->physical_key == 4 && k->logical_key == 'a' && k->pressed)
|
||||||
|
key++;
|
||||||
|
if (k->repeat)
|
||||||
|
repeat++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UHDK_EVENT_EDIT_COMMAND:
|
||||||
|
if (((const uhdk_edit_event *)e->data)->command == UHDK_EDIT_COPY)
|
||||||
|
copy++;
|
||||||
|
break;
|
||||||
|
case UHDK_EVENT_TEXT_COMMIT: {
|
||||||
|
const uhdk_text_event *t = e->data;
|
||||||
|
if (t->text.len == 1 && t->text.ptr[0] == 'a')
|
||||||
|
commit++;
|
||||||
|
if (t->text.len == 2 && memcmp(t->text.ptr, "é", 2) == 0)
|
||||||
|
compose++;
|
||||||
|
if (t->has_replacement) {
|
||||||
|
assert(
|
||||||
|
t->revision == 77 && t->replacement_start == 0 && t->replacement_len == 3
|
||||||
|
);
|
||||||
|
commit++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UHDK_EVENT_TEXT_PREEDIT: {
|
||||||
|
const uhdk_text_event *t = e->data;
|
||||||
|
assert(t->text.len == 2 && memcmp(t->text.ptr, "é", 2) == 0);
|
||||||
|
preedit++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UHDK_EVENT_POINTER_MOVE: {
|
||||||
|
const uhdk_pointer_event *p = e->data;
|
||||||
|
if (p->position.x == 12 && p->position.y == 18)
|
||||||
|
pointer++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UHDK_EVENT_SCROLL: {
|
||||||
|
const uhdk_scroll_event *s = e->data;
|
||||||
|
assert(s->unit == UHDK_SCROLL_LINES && s->delta.y == 0.5f);
|
||||||
|
scroll++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UHDK_EVENT_DROP_FILES: {
|
||||||
|
const uhdk_drop_files_event *d = e->data;
|
||||||
|
assert(d->files.len == 1 && d->position.x == 12);
|
||||||
|
dropped++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"input: key=%u repeat=%u commit=%u preedit=%u pointer=%u scroll=%u drop=%u copy=%u\n",
|
||||||
|
key,
|
||||||
|
repeat,
|
||||||
|
commit,
|
||||||
|
preedit,
|
||||||
|
pointer,
|
||||||
|
scroll,
|
||||||
|
dropped,
|
||||||
|
copy
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
key && repeat && commit >= 2 && preedit && pointer && scroll && dropped && copy && compose
|
||||||
|
);
|
||||||
|
OK(uhdk_clipboard_read_text_async(app, 3, &id));
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_BUFFER_TOO_SMALL);
|
||||||
|
OK(uhdk_clipboard_read_text_async(app, 100, &id));
|
||||||
|
uhdk_request_result r = wait_result(app, id);
|
||||||
|
OK(r.status);
|
||||||
|
const uhdk_text *t = r.data;
|
||||||
|
assert(t->len == 11 && memcmp(t->ptr, "hello café", 11) == 0);
|
||||||
|
OK(uhdk_clipboard_write_text_async(app, text("local copy"), &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
OK(uhdk_window_request_focus_async(w, &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
uhdk_window_destroy(w);
|
||||||
|
puts("input/IME/clipboard/drop/activation: PASS");
|
||||||
|
}
|
||||||
|
static void run(uhdk_app *app, void *mode) {
|
||||||
|
if (strcmp(mode, "input") == 0)
|
||||||
|
input(app);
|
||||||
|
else if (strcmp(mode, "portal") == 0)
|
||||||
|
portals(app);
|
||||||
|
else
|
||||||
|
real_desktop(app);
|
||||||
|
}
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
assert(argc == 2);
|
||||||
|
uhdk_app_desc desc = {0};
|
||||||
|
desc.struct_size = sizeof(desc);
|
||||||
|
desc.abi_version = UHDK_ABI_VERSION;
|
||||||
|
desc.display_name = text("UHDK service tests");
|
||||||
|
desc.application_id = text("org.uhdk.ServiceTests");
|
||||||
|
uhdk_app *app;
|
||||||
|
OK(uhdk_app_create(&desc, &app));
|
||||||
|
OK(uhdk_app_run(app, run, argv[1]));
|
||||||
|
uhdk_app_destroy(app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal isolated Wayland peer for protocol tests, driven by the pinned XML.
|
||||||
|
|
||||||
|
This never attaches to the real compositor or injects input into other clients.
|
||||||
|
It deliberately sends delayed IME batches, selection pipes and copy-only drops.
|
||||||
|
"""
|
||||||
|
import array
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
XML = Path(__file__).resolve().parents[1] / 'protocols/xml'
|
||||||
|
INTERFACES = {}
|
||||||
|
for filename in XML.glob('*.xml'):
|
||||||
|
for interface in ET.parse(filename).getroot().findall('interface'):
|
||||||
|
INTERFACES[interface.get('name')] = interface
|
||||||
|
|
||||||
|
|
||||||
|
def keymap():
|
||||||
|
lib = ctypes.CDLL(ctypes.util.find_library('xkbcommon') or 'libxkbcommon.so.0')
|
||||||
|
lib.xkb_context_new.restype = ctypes.c_void_p
|
||||||
|
lib.xkb_keymap_new_from_names.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
|
||||||
|
lib.xkb_keymap_new_from_names.restype = ctypes.c_void_p
|
||||||
|
lib.xkb_keymap_get_as_string.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||||
|
lib.xkb_keymap_get_as_string.restype = ctypes.c_void_p
|
||||||
|
lib.xkb_context_unref.argtypes = [ctypes.c_void_p]
|
||||||
|
lib.xkb_keymap_unref.argtypes = [ctypes.c_void_p]
|
||||||
|
names = (ctypes.c_char_p * 5)(None, None, b'us', b'intl', None)
|
||||||
|
context = lib.xkb_context_new(0)
|
||||||
|
mapping = lib.xkb_keymap_new_from_names(context, names, 0)
|
||||||
|
assert mapping
|
||||||
|
pointer = lib.xkb_keymap_get_as_string(mapping, 1)
|
||||||
|
result = ctypes.string_at(pointer) + b'\0'
|
||||||
|
libc = ctypes.CDLL(None)
|
||||||
|
libc.free.argtypes = [ctypes.c_void_p]
|
||||||
|
libc.free(pointer)
|
||||||
|
lib.xkb_keymap_unref(mapping)
|
||||||
|
lib.xkb_context_unref(context)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Compositor:
|
||||||
|
globals = [('wl_compositor', 6), ('xdg_wm_base', 6),
|
||||||
|
('zxdg_decoration_manager_v1', 1), ('wl_seat', 8),
|
||||||
|
('wp_cursor_shape_manager_v1', 1), ('zwp_text_input_manager_v3', 1),
|
||||||
|
('wl_data_device_manager', 3), ('zxdg_exporter_v2', 1),
|
||||||
|
('xdg_activation_v1', 1)]
|
||||||
|
|
||||||
|
def __init__(self, sock, inject=False):
|
||||||
|
self.sock = sock
|
||||||
|
self.inject = inject
|
||||||
|
self.buffer = b''
|
||||||
|
self.fds = []
|
||||||
|
self.objects = {1: 'wl_display'}
|
||||||
|
self.ids = {}
|
||||||
|
self.server_id = 0xfeffffff
|
||||||
|
self.surfaces = {}
|
||||||
|
self.serial = 100
|
||||||
|
self.text_serial = 0
|
||||||
|
self.surrounding = ''
|
||||||
|
self.injected = False
|
||||||
|
self.later = None
|
||||||
|
self.tokens = False
|
||||||
|
self.finished_drop = False
|
||||||
|
self.outgoing = None
|
||||||
|
|
||||||
|
def event(self, obj, name, *values):
|
||||||
|
interface = INTERFACES[self.objects[obj]]
|
||||||
|
events = interface.findall('event')
|
||||||
|
description = next(e for e in events if e.get('name') == name)
|
||||||
|
body, fds = b'', []
|
||||||
|
for arg, value in zip(description.findall('arg'), values, strict=True):
|
||||||
|
kind = arg.get('type')
|
||||||
|
if kind in ('uint', 'object', 'new_id'):
|
||||||
|
body += struct.pack('=I', value or 0)
|
||||||
|
elif kind in ('int', 'fixed'):
|
||||||
|
body += struct.pack('=i', value)
|
||||||
|
elif kind in ('string', 'array'):
|
||||||
|
data = (value.encode() + b'\0') if kind == 'string' and value is not None else (value or b'')
|
||||||
|
body += struct.pack('=I', len(data)) + data + b'\0' * (-len(data) % 4)
|
||||||
|
elif kind == 'fd':
|
||||||
|
fds.append(value)
|
||||||
|
else:
|
||||||
|
raise AssertionError(kind)
|
||||||
|
wire = struct.pack('=II', obj, ((len(body) + 8) << 16) | events.index(description)) + body
|
||||||
|
self.sock.sendmsg([wire], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array('i', fds))] if fds else [])
|
||||||
|
|
||||||
|
def new_offer(self, mime):
|
||||||
|
self.server_id += 1
|
||||||
|
obj = self.server_id
|
||||||
|
self.objects[obj] = 'wl_data_offer'
|
||||||
|
self.event(self.ids['wl_data_device'], 'data_offer', obj)
|
||||||
|
self.event(obj, 'offer', mime)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def dispatch(self):
|
||||||
|
try:
|
||||||
|
data, ancillary, _, _ = self.sock.recvmsg(65536, socket.CMSG_SPACE(64 * 4))
|
||||||
|
except ConnectionResetError:
|
||||||
|
return False
|
||||||
|
if not data:
|
||||||
|
return False
|
||||||
|
self.buffer += data
|
||||||
|
for level, kind, payload in ancillary:
|
||||||
|
if level == socket.SOL_SOCKET and kind == socket.SCM_RIGHTS:
|
||||||
|
values = array.array('i'); values.frombytes(payload[:len(payload) // 4 * 4]); self.fds.extend(values)
|
||||||
|
while len(self.buffer) >= 8:
|
||||||
|
obj, header = struct.unpack_from('=II', self.buffer)
|
||||||
|
length, opcode = header >> 16, header & 65535
|
||||||
|
if len(self.buffer) < length:
|
||||||
|
break
|
||||||
|
payload, self.buffer = self.buffer[8:length], self.buffer[length:]
|
||||||
|
iface = self.objects[obj]
|
||||||
|
request = INTERFACES[iface].findall('request')[opcode]
|
||||||
|
args, offset = [], 0
|
||||||
|
for arg in request.findall('arg'):
|
||||||
|
kind = arg.get('type')
|
||||||
|
if kind == 'new_id' and arg.get('interface') is None:
|
||||||
|
size = struct.unpack_from('=I', payload, offset)[0]; offset += 4
|
||||||
|
dynamic = payload[offset:offset + size - 1].decode(); offset += (size + 3) & ~3
|
||||||
|
version, value = struct.unpack_from('=II', payload, offset); offset += 8
|
||||||
|
self.objects[value] = dynamic; self.ids[dynamic] = value
|
||||||
|
args.extend([dynamic, version, value])
|
||||||
|
continue
|
||||||
|
if kind == 'fd':
|
||||||
|
value = self.fds.pop(0)
|
||||||
|
elif kind in ('string', 'array'):
|
||||||
|
size = struct.unpack_from('=I', payload, offset)[0]; offset += 4
|
||||||
|
value = payload[offset:offset + size]; offset += (size + 3) & ~3
|
||||||
|
if kind == 'string': value = value[:-1].decode() if size else None
|
||||||
|
else:
|
||||||
|
value = struct.unpack_from('=i' if kind in ('int', 'fixed') else '=I', payload, offset)[0]; offset += 4
|
||||||
|
if kind == 'new_id':
|
||||||
|
self.objects[value] = arg.get('interface'); self.ids[arg.get('interface')] = value
|
||||||
|
args.append(value)
|
||||||
|
self.request(obj, iface, request.get('name'), args)
|
||||||
|
if request.get('type') == 'destructor':
|
||||||
|
if obj < 0xff000000:
|
||||||
|
self.event(1, 'delete_id', obj)
|
||||||
|
del self.objects[obj]
|
||||||
|
return True
|
||||||
|
|
||||||
|
def request(self, obj, iface, name, args):
|
||||||
|
if iface == 'wl_display' and name == 'get_registry':
|
||||||
|
for index, (interface, version) in enumerate(self.globals, 1): self.event(args[0], 'global', index, interface, version)
|
||||||
|
elif iface == 'wl_display' and name == 'sync':
|
||||||
|
self.event(args[0], 'done', self.serial)
|
||||||
|
self.event(1, 'delete_id', args[0]); del self.objects[args[0]]
|
||||||
|
elif iface == 'wl_registry' and name == 'bind' and args[1] == 'wl_seat':
|
||||||
|
self.event(args[-1], 'capabilities', 3); self.event(args[-1], 'name', 'fixture')
|
||||||
|
elif iface == 'wl_seat' and name == 'get_keyboard':
|
||||||
|
contents = keymap(); fd = os.memfd_create('uhdk-test-keymap'); os.write(fd, contents)
|
||||||
|
self.event(args[0], 'keymap', 1, fd, len(contents)); os.close(fd)
|
||||||
|
self.event(args[0], 'repeat_info', 40, 20)
|
||||||
|
elif iface == 'xdg_wm_base' and name == 'get_xdg_surface':
|
||||||
|
self.surfaces[args[1]] = args[0]
|
||||||
|
elif iface == 'wl_surface' and name == 'commit' and obj in self.surfaces:
|
||||||
|
self.serial += 1; self.event(self.surfaces[obj], 'configure', self.serial)
|
||||||
|
elif iface == 'zxdg_decoration_manager_v1' and name == 'get_toplevel_decoration':
|
||||||
|
self.event(args[0], 'configure', 2)
|
||||||
|
elif iface == 'zwp_text_input_v3' and name == 'set_surrounding_text':
|
||||||
|
self.surrounding = args[0]
|
||||||
|
elif iface == 'zwp_text_input_v3' and name == 'commit':
|
||||||
|
self.text_serial += 1
|
||||||
|
if self.inject and not self.injected and self.surrounding == 'abc': self.start_input(obj)
|
||||||
|
elif iface == 'wl_data_offer' and name == 'receive':
|
||||||
|
mime, fd = args
|
||||||
|
assert not fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK
|
||||||
|
os.write(fd, b'file:///tmp/drop%20file\r\n' if mime == 'text/uri-list' else 'hello café'.encode())
|
||||||
|
os.close(fd)
|
||||||
|
elif iface == 'wl_data_offer' and name == 'finish':
|
||||||
|
self.finished_drop = True
|
||||||
|
elif iface == 'wl_data_device' and name == 'set_selection':
|
||||||
|
source = args[0]
|
||||||
|
closed_read, closed_write = os.pipe()
|
||||||
|
os.close(closed_read)
|
||||||
|
self.event(source, 'send', 'text/plain;charset=utf-8', closed_write)
|
||||||
|
os.close(closed_write)
|
||||||
|
read, write = os.pipe()
|
||||||
|
self.event(source, 'send', 'text/plain;charset=utf-8', write)
|
||||||
|
os.close(write); self.outgoing = read
|
||||||
|
elif iface == 'xdg_activation_token_v1' and name == 'commit':
|
||||||
|
self.event(obj, 'done', 'fixture-token')
|
||||||
|
elif iface == 'xdg_activation_v1' and name == 'activate':
|
||||||
|
assert args[0] == 'fixture-token'; self.tokens = True
|
||||||
|
elif iface == 'zxdg_exporter_v2' and name == 'export_toplevel':
|
||||||
|
self.event(args[0], 'handle', 'fixture-parent')
|
||||||
|
|
||||||
|
def start_input(self, text_input):
|
||||||
|
self.injected = True
|
||||||
|
surface = next(iter(self.surfaces))
|
||||||
|
keyboard, pointer = self.ids['wl_keyboard'], self.ids['wl_pointer']
|
||||||
|
self.event(keyboard, 'enter', 200, surface, b'')
|
||||||
|
self.event(keyboard, 'modifiers', 200, 0, 0, 0, 0)
|
||||||
|
self.event(keyboard, 'key', 201, 1, 30, 1)
|
||||||
|
self.event(keyboard, 'key', 202, 2, 30, 0)
|
||||||
|
self.event(keyboard, 'key', 203, 3, 48, 1)
|
||||||
|
self.event(pointer, 'enter', 204, surface, 10 * 256, 10 * 256)
|
||||||
|
self.event(pointer, 'motion', 4, 12 * 256, 18 * 256)
|
||||||
|
self.event(pointer, 'axis_source', 0)
|
||||||
|
self.event(pointer, 'axis', 5, 0, 5 * 256)
|
||||||
|
self.event(pointer, 'axis_value120', 0, 60)
|
||||||
|
self.event(pointer, 'frame')
|
||||||
|
selection = self.new_offer('text/plain;charset=utf-8')
|
||||||
|
self.event(self.ids['wl_data_device'], 'selection', selection)
|
||||||
|
drop = self.new_offer('text/uri-list')
|
||||||
|
self.event(drop, 'source_actions', 1)
|
||||||
|
self.event(self.ids['wl_data_device'], 'enter', 205, surface, 12 * 256, 18 * 256, drop)
|
||||||
|
self.event(drop, 'action', 1)
|
||||||
|
self.event(self.ids['wl_data_device'], 'drop')
|
||||||
|
self.later = (time.monotonic() + .15, text_input, self.text_serial)
|
||||||
|
|
||||||
|
def timers(self):
|
||||||
|
# Text input enter precedes the application's first surrounding snapshot.
|
||||||
|
text = self.ids.get('zwp_text_input_v3')
|
||||||
|
if text and self.surfaces and not getattr(self, 'text_entered', False):
|
||||||
|
self.text_entered = True
|
||||||
|
self.event(text, 'enter', next(iter(self.surfaces)))
|
||||||
|
if self.later and time.monotonic() >= self.later[0]:
|
||||||
|
_, text, serial = self.later; self.later = None
|
||||||
|
keyboard = self.ids['wl_keyboard']
|
||||||
|
self.event(keyboard, 'key', 206, 6, 48, 0)
|
||||||
|
self.event(keyboard, 'modifiers', 207, 4, 0, 0, 0)
|
||||||
|
self.event(keyboard, 'key', 208, 7, 46, 1)
|
||||||
|
self.event(keyboard, 'key', 209, 8, 46, 0)
|
||||||
|
self.event(keyboard, 'modifiers', 210, 0, 0, 0, 0)
|
||||||
|
self.event(keyboard, 'key', 211, 9, 40, 1)
|
||||||
|
self.event(keyboard, 'key', 212, 10, 40, 0)
|
||||||
|
self.event(keyboard, 'key', 213, 11, 18, 1)
|
||||||
|
self.event(keyboard, 'key', 214, 12, 18, 0)
|
||||||
|
self.event(text, 'preedit_string', 'é', 0, 2)
|
||||||
|
self.event(text, 'done', serial)
|
||||||
|
self.event(text, 'preedit_string', '', 0, 0)
|
||||||
|
self.event(text, 'delete_surrounding_text', 1, 1)
|
||||||
|
self.event(text, 'commit_string', 'λ')
|
||||||
|
self.event(text, 'done', serial)
|
||||||
|
|
||||||
|
|
||||||
|
def run(binary, mode, env=None):
|
||||||
|
client, server = socket.socketpair()
|
||||||
|
environment = dict(os.environ if env is None else env, WAYLAND_SOCKET=str(client.fileno()))
|
||||||
|
if mode == 'input': environment.pop('DBUS_SESSION_BUS_ADDRESS', None)
|
||||||
|
process = subprocess.Popen([str(Path(binary).resolve()), mode], env=environment, pass_fds=(client.fileno(),))
|
||||||
|
client.close()
|
||||||
|
compositor = Compositor(server, mode == 'input')
|
||||||
|
end = time.monotonic() + 20
|
||||||
|
try:
|
||||||
|
while process.poll() is None and time.monotonic() < end:
|
||||||
|
if select.select([server], [], [], .01)[0] and not compositor.dispatch(): break
|
||||||
|
compositor.timers()
|
||||||
|
assert process.wait(timeout=2) == 0, process.returncode
|
||||||
|
if mode == 'input':
|
||||||
|
assert compositor.finished_drop and compositor.tokens
|
||||||
|
assert compositor.outgoing is not None
|
||||||
|
assert os.read(compositor.outgoing, 1024) == b'local copy'
|
||||||
|
finally:
|
||||||
|
if process.poll() is None: process.kill(); process.wait()
|
||||||
|
server.close()
|
||||||
|
for fd in compositor.fds: os.close(fd)
|
||||||
|
if compositor.outgoing is not None: os.close(compositor.outgoing)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run(sys.argv[1], 'input')
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/* Pinned xkbcommon ABI; libraries.c checks these against upstream typeof. */
|
||||||
|
extern struct xkb_context *(*z_xkb_context_new)(enum xkb_context_flags flags);
|
||||||
|
extern void (*z_xkb_context_unref)(struct xkb_context *context);
|
||||||
|
extern struct xkb_keymap *(*z_xkb_keymap_new_from_string)(
|
||||||
|
struct xkb_context *context,
|
||||||
|
const char *string,
|
||||||
|
enum xkb_keymap_format format,
|
||||||
|
enum xkb_keymap_compile_flags flags
|
||||||
|
);
|
||||||
|
extern void (*z_xkb_keymap_unref)(struct xkb_keymap *keymap);
|
||||||
|
extern int (*z_xkb_keymap_key_repeats)(struct xkb_keymap *keymap, xkb_keycode_t key);
|
||||||
|
extern struct xkb_state *(*z_xkb_state_new)(struct xkb_keymap *keymap);
|
||||||
|
extern void (*z_xkb_state_unref)(struct xkb_state *state);
|
||||||
|
extern enum xkb_state_component (*z_xkb_state_update_mask)(
|
||||||
|
struct xkb_state *state,
|
||||||
|
xkb_mod_mask_t depressed_mods,
|
||||||
|
xkb_mod_mask_t latched_mods,
|
||||||
|
xkb_mod_mask_t locked_mods,
|
||||||
|
xkb_layout_index_t depressed_layout,
|
||||||
|
xkb_layout_index_t latched_layout,
|
||||||
|
xkb_layout_index_t locked_layout
|
||||||
|
);
|
||||||
|
extern xkb_keysym_t (*z_xkb_state_key_get_one_sym)(struct xkb_state *state, xkb_keycode_t key);
|
||||||
|
extern int (*z_xkb_state_key_get_utf8)(
|
||||||
|
struct xkb_state *state,
|
||||||
|
xkb_keycode_t key,
|
||||||
|
char *buffer,
|
||||||
|
size_t size
|
||||||
|
);
|
||||||
|
extern int (*z_xkb_state_mod_name_is_active)(
|
||||||
|
struct xkb_state *state,
|
||||||
|
const char *name,
|
||||||
|
enum xkb_state_component type
|
||||||
|
);
|
||||||
|
extern uint32_t (*z_xkb_keysym_to_utf32)(xkb_keysym_t keysym);
|
||||||
|
extern struct xkb_compose_table *(*z_xkb_compose_table_new_from_locale)(
|
||||||
|
struct xkb_context *context,
|
||||||
|
const char *locale,
|
||||||
|
enum xkb_compose_compile_flags flags
|
||||||
|
);
|
||||||
|
extern void (*z_xkb_compose_table_unref)(struct xkb_compose_table *table);
|
||||||
|
extern struct xkb_compose_state *(*z_xkb_compose_state_new)(
|
||||||
|
struct xkb_compose_table *table,
|
||||||
|
enum xkb_compose_state_flags flags
|
||||||
|
);
|
||||||
|
extern void (*z_xkb_compose_state_unref)(struct xkb_compose_state *state);
|
||||||
|
extern enum xkb_compose_feed_result (*z_xkb_compose_state_feed)(
|
||||||
|
struct xkb_compose_state *state,
|
||||||
|
xkb_keysym_t keysym
|
||||||
|
);
|
||||||
|
extern void (*z_xkb_compose_state_reset)(struct xkb_compose_state *state);
|
||||||
|
extern enum xkb_compose_status (*z_xkb_compose_state_get_status)(struct xkb_compose_state *state);
|
||||||
|
extern int (*z_xkb_compose_state_get_utf8)(
|
||||||
|
struct xkb_compose_state *state,
|
||||||
|
char *buffer,
|
||||||
|
size_t size
|
||||||
|
);
|
||||||
|
|
@ -19,7 +19,13 @@ test {
|
||||||
std.testing.refAllDecls(@import("FileOperation.zig"));
|
std.testing.refAllDecls(@import("FileOperation.zig"));
|
||||||
_ = @import("core_tests.zig");
|
_ = @import("core_tests.zig");
|
||||||
_ = @import("arenas.zig");
|
_ = @import("arenas.zig");
|
||||||
if (@import("builtin").os.tag == .linux) _ = @import("linux/Files.zig");
|
if (@import("builtin").os.tag == .linux) {
|
||||||
|
_ = @import("linux/Files.zig");
|
||||||
|
_ = @import("linux/Keyboard.zig");
|
||||||
|
_ = @import("linux/Text.zig");
|
||||||
|
_ = @import("linux/Watch.zig");
|
||||||
|
_ = @import("linux/Desktop.zig");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checkExports(comptime module: type) void {
|
fn checkExports(comptime module: type) void {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue