Harden input ordering, portal activation and portable Linux builds
This commit is contained in:
parent
5623376284
commit
c1c2de8c4b
|
|
@ -323,10 +323,6 @@ pub fn forget(w: *native.Window) void {
|
|||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@ const Keyboard = @import("Keyboard.zig");
|
|||
const Data = @import("Data.zig");
|
||||
const Text = @import("Text.zig");
|
||||
|
||||
const Key = struct {
|
||||
code: u32,
|
||||
down: bool,
|
||||
repeated: bool,
|
||||
sym: u32,
|
||||
modifiers: u32,
|
||||
text: [256]u8 = undefined,
|
||||
len: usize = 0,
|
||||
};
|
||||
pub const State = struct {
|
||||
seat: ?*c.struct_wl_seat = null,
|
||||
seat_name: u32 = 0,
|
||||
|
|
@ -29,6 +38,8 @@ pub const State = struct {
|
|||
compose: ?*c.struct_xkb_compose_state = null,
|
||||
modifiers: u32 = 0,
|
||||
active: bool = false,
|
||||
pending_key: ?Key = null,
|
||||
key_sync: ?*c.struct_wl_callback = null,
|
||||
serial: u32 = 0,
|
||||
serial_time: u64 = 0,
|
||||
pointer_serial: u32 = 0,
|
||||
|
|
@ -137,6 +148,7 @@ fn seatCapabilities(_: ?*anyopaque, seat: ?*c.struct_wl_seat, caps: u32) callcon
|
|||
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 {
|
||||
flushKey();
|
||||
defer _ = c.close(fd);
|
||||
const s = state();
|
||||
s.repeat_key = null;
|
||||
|
|
@ -182,31 +194,66 @@ fn modifiers(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, depressed: u32,
|
|||
const s = state();
|
||||
if (s.state != null) _ = c.z_xkb_state_update_mask.?(s.state, depressed, latched, locked, 0, 0, group);
|
||||
updateModifiers();
|
||||
flushKey();
|
||||
}
|
||||
fn key(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, serial: u32, _: u32, code: u32, pressed: u32) callconv(.c) void {
|
||||
const s = state();
|
||||
flushKey();
|
||||
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);
|
||||
s.pending_key = prepareKey(code, pressed != 0, false);
|
||||
// wl_keyboard has no frame event. A sync boundary lets the following
|
||||
// authoritative modifiers event supply the public post-event flags even
|
||||
// when the key and modifier messages arrive in separate socket reads.
|
||||
if (s.key_sync == null) {
|
||||
s.key_sync = c.wl_display_sync(b().display);
|
||||
if (s.key_sync != null) _ = c.wl_callback_add_listener(s.key_sync, &key_sync_listener, null) else flushKey();
|
||||
}
|
||||
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 prepareKey(code: u32, down: bool, repeated: bool) Key {
|
||||
const s = state();
|
||||
var value: Key = .{ .code = code, .down = down, .repeated = repeated, .sym = if (s.state != null) c.z_xkb_state_key_get_one_sym.?(s.state, code + 8) else 0, .modifiers = s.modifiers };
|
||||
if (down and s.state != null) {
|
||||
const length = c.z_xkb_state_key_get_utf8.?(s.state, code + 8, &value.text, value.text.len);
|
||||
if (length > 0 and length < value.text.len) value.len = @intCast(length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
fn deliverKey(code: u32, down: bool, repeated: bool) void {
|
||||
emitKey(prepareKey(code, down, repeated));
|
||||
}
|
||||
fn emitKey(value: Key) 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) };
|
||||
event.data.key = .{ .physical_key = Keyboard.physical(value.code), .logical_key = if (s.state != null) Keyboard.logical(value.sym) else 0, .modifiers = s.modifiers, .pressed = @intFromBool(value.down), .repeat = @intFromBool(value.repeated) };
|
||||
send(w, &event);
|
||||
if (down and w.text.enabled) Text.key(w, code, sym, event.data.key.logical_key, event.input_id);
|
||||
// Resolve symbols/text before consuming a latched modifier, while reporting
|
||||
// the compositor's post-event flags in the observational raw event.
|
||||
if (value.down and w.text.enabled) Text.key(w, value.sym, event.data.key.logical_key, event.input_id, value.modifiers, value.text[0..value.len]);
|
||||
}
|
||||
pub fn flushKey() void {
|
||||
if (state().pending_key) |value| {
|
||||
state().pending_key = null;
|
||||
emitKey(value);
|
||||
}
|
||||
}
|
||||
fn keySynced(_: ?*anyopaque, callback: ?*c.struct_wl_callback, _: u32) callconv(.c) void {
|
||||
c.wl_callback_destroy(callback);
|
||||
state().key_sync = null;
|
||||
flushKey();
|
||||
}
|
||||
const key_sync_listener: c.struct_wl_callback_listener = .{ .done = keySynced };
|
||||
|
||||
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));
|
||||
|
|
@ -216,7 +263,6 @@ const keyboard_listener: c.struct_wl_keyboard_listener = .{ .keymap = keymap, .e
|
|||
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);
|
||||
|
|
@ -224,6 +270,7 @@ pub fn tick() i32 {
|
|||
event.data.active = @intFromBool(active);
|
||||
n.z_emit(runtime.toAppHandle(b().app), &event);
|
||||
}
|
||||
if (s.pending_key != null) return -1;
|
||||
if (s.repeat_key) |code| {
|
||||
const now = runtime.now(b().app.io);
|
||||
if (now >= s.repeat_at) {
|
||||
|
|
@ -235,7 +282,9 @@ pub fn tick() i32 {
|
|||
return -1;
|
||||
}
|
||||
fn leaveKeyboard() void {
|
||||
flushKey();
|
||||
const s = state();
|
||||
s.modifiers = 0;
|
||||
s.repeat_key = null;
|
||||
if (s.key_focus) |w| {
|
||||
for (&s.held, 0..) |*held, code| if (held.*) {
|
||||
|
|
@ -402,6 +451,8 @@ pub fn remove(name: u32) void {
|
|||
fn releaseSeat() void {
|
||||
Data.releaseSeat();
|
||||
const s = state();
|
||||
if (s.key_sync != null) c.wl_callback_destroy(s.key_sync);
|
||||
s.key_sync = null;
|
||||
leaveKeyboard();
|
||||
leavePointer();
|
||||
if (s.text_focus) |w| Text.forget(w);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const n = @import("native");
|
|||
const native = @import("native.zig");
|
||||
const Bus = @import("Bus.zig");
|
||||
const Files = @import("Files.zig");
|
||||
const Input = @import("Input.zig");
|
||||
const runtime = @import("../runtime.zig");
|
||||
const abi = @import("../abi.zig");
|
||||
const desktop = "org.freedesktop.portal.Desktop";
|
||||
|
|
@ -24,6 +25,7 @@ const Pending = struct {
|
|||
handle: [:0]u8,
|
||||
token: [:0]u8,
|
||||
exported: ?*c.struct_zxdg_exported_v2 = null,
|
||||
activation: ?*c.struct_xdg_activation_token_v1 = null,
|
||||
deadline: u64,
|
||||
};
|
||||
pub const State = struct {
|
||||
|
|
@ -309,7 +311,18 @@ pub fn request(cmd: *const n.z_command, id: u64) u32 {
|
|||
return abi.OOM;
|
||||
};
|
||||
_ = c.zxdg_exported_v2_add_listener(p.exported, &export_listener, p);
|
||||
} else submit(p, "") catch {
|
||||
} else if (cmd.op == n.Z_URL and b().desktop.activation != null and Input.state().key_focus != null and
|
||||
Input.state().serial != 0 and runtime.now(b().app.io) -| Input.state().serial_time <= 5 * std.time.ns_per_s)
|
||||
{
|
||||
p.activation = c.xdg_activation_v1_get_activation_token(b().desktop.activation) orelse {
|
||||
destroy(p);
|
||||
return abi.OOM;
|
||||
};
|
||||
_ = c.xdg_activation_token_v1_add_listener(p.activation, &activation_listener, p);
|
||||
c.xdg_activation_token_v1_set_serial(p.activation, Input.state().serial, Input.state().seat);
|
||||
c.xdg_activation_token_v1_set_surface(p.activation, Input.state().key_focus.?.surface);
|
||||
c.xdg_activation_token_v1_commit(p.activation);
|
||||
} else submit(p, "", null) catch {
|
||||
destroy(p);
|
||||
return abi.OOM;
|
||||
};
|
||||
|
|
@ -336,13 +349,21 @@ fn exported(raw: ?*anyopaque, _: ?*c.struct_zxdg_exported_v2, handle: [*c]const
|
|||
return;
|
||||
};
|
||||
defer b().app.gpa.free(parent);
|
||||
submit(p, parent) catch {
|
||||
submit(p, parent, null) 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 {
|
||||
fn activated(raw: ?*anyopaque, _: ?*c.struct_xdg_activation_token_v1, token: [*c]const u8) callconv(.c) void {
|
||||
const p: *Pending = @ptrCast(@alignCast(raw.?));
|
||||
submit(p, "", token) catch {
|
||||
complete(p, abi.OOM, null);
|
||||
destroy(p);
|
||||
};
|
||||
}
|
||||
const activation_listener: c.struct_xdg_activation_token_v1_listener = .{ .done = activated };
|
||||
fn submit(p: *Pending, parent: [*:0]const u8, activation_token: ?[*: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");
|
||||
|
|
@ -356,6 +377,7 @@ fn submit(p: *Pending, parent: [*:0]const u8) !void {
|
|||
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 (activation_token) |token| try Bus.option(&options, "activation_token", "s", token);
|
||||
if (dialog) {
|
||||
try Bus.option(&options, "modal", "b", @as(u32, @intFromBool(cmd.dialog.parent != null)));
|
||||
if (cmd.dialog.kind != n.UHDK_DIALOG_SAVE_FILE) {
|
||||
|
|
@ -485,6 +507,7 @@ fn destroy(p: *Pending) void {
|
|||
while (link.* != p) link = &link.*.?.next;
|
||||
link.* = p.next;
|
||||
if (p.exported != null) c.zxdg_exported_v2_destroy(p.exported);
|
||||
if (p.activation != null) c.xdg_activation_token_v1_destroy(p.activation);
|
||||
b().app.gpa.free(p.handle);
|
||||
b().app.gpa.free(p.token);
|
||||
b().app.gpa.destroy(p);
|
||||
|
|
|
|||
|
|
@ -114,7 +114,10 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
|||
|
||||
- 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.
|
||||
and locale-aware compose tables. Raw events use the post-event modifier mask;
|
||||
text lookup retains the pre-event state for latched modifiers. A Wayland sync
|
||||
boundary flushes keys without modifier changes. 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
|
||||
|
|
@ -138,7 +141,8 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
|||
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.
|
||||
Connection/method setup is bounded; human interaction has no timeout. URL
|
||||
handoff includes an activation token when recent focused input permits it.
|
||||
- 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)
|
||||
|
|
@ -178,9 +182,9 @@ 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
|
||||
clipboard size limits/cancellation/invalid UTF-8/early reader exit, URI drops,
|
||||
activation, portal open/save options, live settings, 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
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ fn deleted(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, before: u32, after:
|
|||
w.text.after = after;
|
||||
}
|
||||
fn done(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, serial: u32) callconv(.c) void {
|
||||
Input.flushKey();
|
||||
const w = Input.state().text_focus orelse return;
|
||||
const t = &w.text;
|
||||
defer clearPending(w);
|
||||
|
|
@ -276,25 +277,26 @@ 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 {
|
||||
pub fn key(w: *native.Window, sym: u32, logical: u32, input_id: u64, modifiers: u32, plain: []const u8) 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);
|
||||
const edit = Keyboard.edit(logical, 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) };
|
||||
e.data.edit = .{ .command = edit, .extend_selection = @intFromBool(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;
|
||||
if (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;
|
||||
var length: c_int = @intCast(plain.len);
|
||||
@memcpy(buffer[0..plain.len], plain);
|
||||
if (s.compose != null) {
|
||||
_ = c.z_xkb_compose_state_feed.?(s.compose, sym);
|
||||
switch (c.z_xkb_compose_state_get_status.?(s.compose)) {
|
||||
|
|
@ -312,9 +314,9 @@ pub fn key(w: *native.Window, code: u32, sym: u32, logical: u32, input_id: u64)
|
|||
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 => {},
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include "wayland-client-core.h"
|
||||
#include "wayland-egl-core.h"
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ async def main(binary):
|
|||
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)
|
||||
if is_url:
|
||||
for key, value in [('color-scheme', 2), ('contrast', 0), ('reduced-motion', 0)]:
|
||||
bus.send(Message.new_signal(PATH, 'org.freedesktop.portal.Settings', 'SettingChanged', 'ssv', ['org.freedesktop.appearance', key, Variant('u', value)]))
|
||||
response(handle)
|
||||
|
||||
async def restart():
|
||||
await bus.release_name(DESKTOP)
|
||||
|
|
@ -65,11 +68,13 @@ async def main(binary):
|
|||
handle_path = PATH + '/request/' + message.sender[1:].replace('.', '_') + '/' + token
|
||||
if message.member == 'OpenURI':
|
||||
assert title == 'https://example.org/test'
|
||||
assert options['activation_token'].value == 'fixture-token'
|
||||
schedule(delayed_reply(message, handle_path, .35, True))
|
||||
seen.add('url')
|
||||
return True
|
||||
seen.add(title)
|
||||
assert message.signature == 'ssa{sv}'
|
||||
if message.member == 'OpenFile':
|
||||
assert options['multiple'].value and not options['directory'].value
|
||||
assert options['filters'].value == [['Text', [[0, '*.txt'], [0, '*.md']]]]
|
||||
if title == 'success':
|
||||
|
|
@ -79,6 +84,13 @@ async def main(binary):
|
|||
# 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 == 'save':
|
||||
assert message.member == 'SaveFile'
|
||||
assert options['current_name'].value == 'book.txt'
|
||||
assert options['accept_label'].value == 'Store'
|
||||
assert bytes(options['current_folder'].value) == b'/tmp\0'
|
||||
bus.send(Message.new_method_return(message, 'o', [handle_path]))
|
||||
response(handle_path, results={'uris': Variant('as', ['file:///tmp/save.txt'])})
|
||||
elif title == 'cancel':
|
||||
# Return a different handle after local cancellation; both
|
||||
# the predicted and returned paths need Close calls.
|
||||
|
|
@ -102,7 +114,7 @@ async def main(binary):
|
|||
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 {'success', 'save', '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')
|
||||
|
|
|
|||
|
|
@ -162,6 +162,28 @@ static void portals(uhdk_app *app) {
|
|||
files->ptr[0], UHDK_FILE_NATIVE_PATH, bytes, sizeof(bytes), &len
|
||||
));
|
||||
assert(len == 14 && memcmp(bytes, "/tmp/a b%\xff.txt", 14) == 0);
|
||||
uhdk_preferences prefs = {0};
|
||||
prefs.struct_size = sizeof(prefs);
|
||||
OK(uhdk_app_get_preferences(app, &prefs));
|
||||
assert(prefs.appearance == UHDK_APPEARANCE_DARK && prefs.reduced_motion && prefs.high_contrast);
|
||||
uhdk_file *directory;
|
||||
OK(
|
||||
uhdk_file_create_from_native_path(app, (uhdk_bytes){(const uint8_t *)"/tmp", 4}, &directory)
|
||||
);
|
||||
desc.title = text("save");
|
||||
desc.kind = UHDK_DIALOG_SAVE_FILE;
|
||||
desc.suggested_name = text("book.txt");
|
||||
desc.accept_label = text("Store");
|
||||
desc.initial_directory = directory;
|
||||
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||
r = wait_result(app, id);
|
||||
OK(r.status);
|
||||
assert(((const uhdk_file_list *)r.data)->len == 1);
|
||||
uhdk_file_release(directory);
|
||||
desc.kind = UHDK_DIALOG_OPEN_FILES;
|
||||
desc.initial_directory = NULL;
|
||||
desc.suggested_name = text("");
|
||||
desc.accept_label = text("");
|
||||
desc.title = text("cancel");
|
||||
OK(uhdk_file_dialog_show_async(app, &desc, &id));
|
||||
// Allow method submission, then cancel while the mock withholds its reply.
|
||||
|
|
@ -169,12 +191,17 @@ static void portals(uhdk_app *app) {
|
|||
OK(uhdk_app_poll_events(app, 100000000, &events));
|
||||
OK(uhdk_request_cancel(app, id));
|
||||
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);
|
||||
OK(uhdk_app_get_preferences(app, &prefs));
|
||||
assert(
|
||||
prefs.appearance == UHDK_APPEARANCE_LIGHT && !prefs.reduced_motion && !prefs.high_contrast
|
||||
);
|
||||
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));
|
||||
|
|
@ -200,19 +227,28 @@ static void input(uhdk_app *app) {
|
|||
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;
|
||||
copy = 0, compose = 0, captured = 0, released = 0;
|
||||
uint64_t acknowledged = 0, capture_request = 0;
|
||||
int published = 0;
|
||||
uint64_t end = deadline(app);
|
||||
while (uhdk_app_get_time_ns(app) < end && !(key && repeat && commit >= 2 && preedit &&
|
||||
pointer && scroll && dropped && copy && compose)) {
|
||||
while (uhdk_app_get_time_ns(app) < end &&
|
||||
!(key && repeat && commit >= 2 && preedit && pointer && scroll && dropped && copy &&
|
||||
compose && captured && released)) {
|
||||
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];
|
||||
if (e->input_id > acknowledged)
|
||||
acknowledged = e->input_id;
|
||||
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->physical_key == 224)
|
||||
assert(!!(k->modifiers & UHDK_MOD_CONTROL) == !!k->pressed);
|
||||
if (k->logical_key == 'A' && k->pressed)
|
||||
assert(!(k->modifiers & UHDK_MOD_SHIFT));
|
||||
if (k->repeat)
|
||||
repeat++;
|
||||
break;
|
||||
|
|
@ -241,6 +277,22 @@ static void input(uhdk_app *app) {
|
|||
preedit++;
|
||||
break;
|
||||
}
|
||||
case UHDK_EVENT_POINTER_BUTTON:
|
||||
if (((const uhdk_pointer_event *)e->data)->pressed)
|
||||
OK(uhdk_window_set_pointer_mode_async(
|
||||
w, UHDK_POINTER_CAPTURE, &capture_request
|
||||
));
|
||||
break;
|
||||
case UHDK_EVENT_POINTER_MODE_CHANGED:
|
||||
if (((const uhdk_pointer_mode_event *)e->data)->mode == UHDK_POINTER_CAPTURE)
|
||||
captured++;
|
||||
else
|
||||
released++;
|
||||
break;
|
||||
case UHDK_EVENT_REQUEST_DONE:
|
||||
if (((const uhdk_request_result *)e->data)->id == capture_request)
|
||||
OK(((const uhdk_request_result *)e->data)->status);
|
||||
break;
|
||||
case UHDK_EVENT_POINTER_MOVE: {
|
||||
const uhdk_pointer_event *p = e->data;
|
||||
if (p->position.x == 12 && p->position.y == 18)
|
||||
|
|
@ -263,6 +315,13 @@ static void input(uhdk_app *app) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (repeat && !published) {
|
||||
state.surrounding_text = text("abcd");
|
||||
state.revision = 78;
|
||||
state.acknowledged_input_id = acknowledged;
|
||||
OK(uhdk_window_set_text_state(w, &state));
|
||||
published = 1;
|
||||
}
|
||||
}
|
||||
fprintf(
|
||||
stderr,
|
||||
|
|
@ -286,6 +345,17 @@ static void input(uhdk_app *app) {
|
|||
OK(r.status);
|
||||
const uhdk_text *t = r.data;
|
||||
assert(t->len == 11 && memcmp(t->ptr, "hello café", 11) == 0);
|
||||
OK(uhdk_clipboard_read_text_async(app, 100, &id));
|
||||
uhdk_event_list pause;
|
||||
OK(uhdk_app_poll_events(app, 100000000, &pause));
|
||||
OK(uhdk_request_cancel(app, id));
|
||||
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
|
||||
OK(uhdk_clipboard_read_text_async(app, 100, &id));
|
||||
assert(wait_result(app, id).status == UHDK_STATUS_INVALID_ARGUMENT);
|
||||
OK(uhdk_clipboard_read_text_async(app, 0, &id));
|
||||
r = wait_result(app, id);
|
||||
OK(r.status);
|
||||
assert(((const uhdk_text *)r.data)->len == 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));
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ class Compositor:
|
|||
self.tokens = False
|
||||
self.finished_drop = False
|
||||
self.outgoing = None
|
||||
self.receives = 0
|
||||
self.held = []
|
||||
|
||||
def event(self, obj, name, *values):
|
||||
interface = INTERFACES[self.objects[obj]]
|
||||
|
|
@ -176,7 +178,15 @@ class Compositor:
|
|||
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())
|
||||
if mime == 'text/uri-list':
|
||||
payload = b'file:///tmp/drop%20file\r\n'
|
||||
else:
|
||||
self.receives += 1
|
||||
if self.receives == 3:
|
||||
self.held.append(fd)
|
||||
return
|
||||
payload = b'\xff' if self.receives == 4 else b'' if self.receives == 5 else 'hello café'.encode()
|
||||
os.write(fd, payload)
|
||||
os.close(fd)
|
||||
elif iface == 'wl_data_offer' and name == 'finish':
|
||||
self.finished_drop = True
|
||||
|
|
@ -207,6 +217,7 @@ class Compositor:
|
|||
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, 'button', 204, 4, 0x110, 1)
|
||||
self.event(pointer, 'axis_source', 0)
|
||||
self.event(pointer, 'axis', 5, 0, 5 * 256)
|
||||
self.event(pointer, 'axis_value120', 0, 60)
|
||||
|
|
@ -225,15 +236,29 @@ class Compositor:
|
|||
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)))
|
||||
surface = next(iter(self.surfaces))
|
||||
self.event(text, 'enter', surface)
|
||||
if not self.inject:
|
||||
keyboard = self.ids['wl_keyboard']
|
||||
self.event(keyboard, 'enter', 190, surface, b'')
|
||||
self.event(keyboard, 'key', 191, 1, 30, 1)
|
||||
self.event(keyboard, 'key', 192, 2, 30, 0)
|
||||
if self.later and time.monotonic() >= self.later[0]:
|
||||
_, text, serial = self.later; self.later = None
|
||||
assert self.text_serial > serial, (self.text_serial, serial)
|
||||
self.event(self.ids['wl_pointer'], 'button', 206, 6, 0x110, 0)
|
||||
keyboard = self.ids['wl_keyboard']
|
||||
self.event(keyboard, 'key', 206, 6, 48, 0)
|
||||
self.event(keyboard, 'key', 207, 7, 29, 1)
|
||||
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, 'key', 210, 9, 29, 0)
|
||||
self.event(keyboard, 'modifiers', 210, 0, 0, 0, 0)
|
||||
self.event(keyboard, 'modifiers', 210, 0, 1, 0, 0)
|
||||
self.event(keyboard, 'key', 210, 9, 30, 1)
|
||||
self.event(keyboard, 'modifiers', 210, 0, 0, 0, 0)
|
||||
self.event(keyboard, 'key', 210, 9, 30, 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)
|
||||
|
|
@ -266,7 +291,7 @@ def run(binary, mode, env=None):
|
|||
finally:
|
||||
if process.poll() is None: process.kill(); process.wait()
|
||||
server.close()
|
||||
for fd in compositor.fds: os.close(fd)
|
||||
for fd in compositor.fds + compositor.held: os.close(fd)
|
||||
if compositor.outgoing is not None: os.close(compositor.outgoing)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue