Implement Linux software context menus on Wayland
This commit is contained in:
parent
0d2c8eedf9
commit
85acfb5b67
|
|
@ -12,8 +12,9 @@ Windows 10 version 1703 or later and an OpenGL 3.3-capable driver.
|
||||||
The Linux Wayland preview implements windows, EGL/OpenGL, keyboard/pointer and
|
The Linux Wayland preview implements windows, EGL/OpenGL, keyboard/pointer and
|
||||||
text input, clipboard/file drops, portal dialogs/preferences, activation, icons,
|
text input, clipboard/file drops, portal dialogs/preferences, activation, icons,
|
||||||
file watches, and Plasma app/window menu export, initially tested on KDE Plasma.
|
file watches, and Plasma app/window menu export, initially tested on KDE Plasma.
|
||||||
Native context menus and X11 remain unsupported. It vendors only ABI
|
Linux context menus use a small software renderer and a bundled 16px Roboto
|
||||||
headers and protocol bindings and loads system libraries at runtime. Normal
|
bitmap; X11 remains unsupported. Desktop dependencies are limited to vendored
|
||||||
|
ABI headers/protocol bindings and runtime-loaded system libraries. 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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const n = @import("native");
|
||||||
const native = @import("native.zig");
|
const native = @import("native.zig");
|
||||||
const runtime = @import("../runtime.zig");
|
const runtime = @import("../runtime.zig");
|
||||||
const abi = @import("../abi.zig");
|
const abi = @import("../abi.zig");
|
||||||
|
const Popup = @import("Popup.zig");
|
||||||
const KdeAppMenu = @import("KdeAppMenu.zig");
|
const KdeAppMenu = @import("KdeAppMenu.zig");
|
||||||
const Keyboard = @import("Keyboard.zig");
|
const Keyboard = @import("Keyboard.zig");
|
||||||
const Data = @import("Data.zig");
|
const Data = @import("Data.zig");
|
||||||
|
|
@ -43,6 +44,10 @@ pub const State = struct {
|
||||||
key_sync: ?*c.struct_wl_callback = null,
|
key_sync: ?*c.struct_wl_callback = null,
|
||||||
serial: u32 = 0,
|
serial: u32 = 0,
|
||||||
serial_time: u64 = 0,
|
serial_time: u64 = 0,
|
||||||
|
popup_serial: u32 = 0,
|
||||||
|
popup_time: u64 = 0,
|
||||||
|
popup_window: ?*native.Window = null,
|
||||||
|
popup_buttons: u32 = 0,
|
||||||
pointer_serial: u32 = 0,
|
pointer_serial: u32 = 0,
|
||||||
next_id: u64 = 0,
|
next_id: u64 = 0,
|
||||||
position: n.uhdk_point = .{ .x = 0, .y = 0 },
|
position: n.uhdk_point = .{ .x = 0, .y = 0 },
|
||||||
|
|
@ -122,6 +127,7 @@ pub fn setup() void {
|
||||||
if (s.text_input != null) Text.listen(s.text_input);
|
if (s.text_input != null) Text.listen(s.text_input);
|
||||||
}
|
}
|
||||||
Data.setup();
|
Data.setup();
|
||||||
|
Popup.capability();
|
||||||
native.setCapability(n.UHDK_CAP_POINTER_CAPTURE, s.pointer != null);
|
native.setCapability(n.UHDK_CAP_POINTER_CAPTURE, s.pointer != null);
|
||||||
native.setCapability(n.UHDK_CAP_TEXT_INPUT, s.context != null and s.keyboard != null);
|
native.setCapability(n.UHDK_CAP_TEXT_INPUT, s.context != null and s.keyboard != null);
|
||||||
}
|
}
|
||||||
|
|
@ -172,11 +178,16 @@ fn keymap(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, format: u32, fd: i32, size:
|
||||||
fn keyEnter(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, surface: ?*c.struct_wl_surface, keys: [*c]c.struct_wl_array) callconv(.c) void {
|
fn keyEnter(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, surface: ?*c.struct_wl_surface, keys: [*c]c.struct_wl_array) callconv(.c) void {
|
||||||
leaveKeyboard();
|
leaveKeyboard();
|
||||||
const s = state();
|
const s = state();
|
||||||
s.key_focus = find(surface);
|
s.key_focus = Popup.owner(surface) orelse find(surface);
|
||||||
|
const suppressed = s.menu_keys;
|
||||||
|
s.menu_keys = @splat(false);
|
||||||
if (s.state != null and keys.*.size != 0) {
|
if (s.state != null and keys.*.size != 0) {
|
||||||
const held: [*]const u32 = @ptrCast(@alignCast(keys.*.data));
|
const held: [*]const u32 = @ptrCast(@alignCast(keys.*.data));
|
||||||
for (held[0 .. keys.*.size / 4]) |code| {
|
for (held[0 .. keys.*.size / 4]) |code| {
|
||||||
if (code < s.held.len) s.held[code] = true;
|
if (code < s.held.len) {
|
||||||
|
s.held[code] = true;
|
||||||
|
s.menu_keys[code] = suppressed[code];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +214,7 @@ fn key(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, serial: u32, _: u32, code: u32
|
||||||
flushKey();
|
flushKey();
|
||||||
if (code > 0xffff) return;
|
if (code > 0xffff) return;
|
||||||
provenance(serial);
|
provenance(serial);
|
||||||
|
if (pressed != 0 and !Popup.active()) popupTrigger(s.key_focus, serial);
|
||||||
if (code < s.held.len) s.held[code] = pressed != 0;
|
if (code < s.held.len) s.held[code] = pressed != 0;
|
||||||
// The compositor supplies the authoritative modifier/group mask. Running
|
// The compositor supplies the authoritative modifier/group mask. Running
|
||||||
// XKB key actions locally as well would apply latch/lock actions twice.
|
// XKB key actions locally as well would apply latch/lock actions twice.
|
||||||
|
|
@ -240,14 +252,20 @@ fn emitKey(value: Key) void {
|
||||||
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) };
|
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) };
|
||||||
if (value.code < s.menu_keys.len) {
|
if (value.code < s.menu_keys.len) {
|
||||||
if (s.menu_keys[value.code]) {
|
if (s.menu_keys[value.code]) {
|
||||||
if (!value.down) s.menu_keys[value.code] = false;
|
if (!value.down) s.menu_keys[value.code] = false else if (Popup.active()) Popup.key(event.data.key.logical_key, true, s.serial);
|
||||||
return; // Consume the release/repeats even after flags or focus change.
|
return; // Consume the release/repeats even after flags or focus change.
|
||||||
}
|
}
|
||||||
|
if (Popup.active()) {
|
||||||
|
s.menu_keys[value.code] = value.down;
|
||||||
|
Popup.key(event.data.key.logical_key, value.down, s.serial);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (value.down and !value.repeated and KdeAppMenu.shortcut(w, event.data.key.logical_key, value.modifiers, event.input_id)) {
|
if (value.down and !value.repeated and KdeAppMenu.shortcut(w, event.data.key.logical_key, value.modifiers, event.input_id)) {
|
||||||
s.menu_keys[value.code] = true;
|
s.menu_keys[value.code] = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Popup.active()) return;
|
||||||
send(w, &event);
|
send(w, &event);
|
||||||
// Resolve symbols/text before consuming a latched modifier, while reporting
|
// Resolve symbols/text before consuming a latched modifier, while reporting
|
||||||
// the compositor's post-event flags in the observational raw event.
|
// the compositor's post-event flags in the observational raw event.
|
||||||
|
|
@ -299,20 +317,22 @@ fn leaveKeyboard() void {
|
||||||
s.modifiers = 0;
|
s.modifiers = 0;
|
||||||
s.repeat_key = null;
|
s.repeat_key = null;
|
||||||
if (s.key_focus) |w| {
|
if (s.key_focus) |w| {
|
||||||
|
if (!Popup.active()) clearPopupTrigger(w);
|
||||||
for (&s.held, 0..) |*held, code| if (held.*) {
|
for (&s.held, 0..) |*held, code| if (held.*) {
|
||||||
held.* = false;
|
held.* = false;
|
||||||
deliverKey(@intCast(code), false, false);
|
if (!s.menu_keys[code]) deliverKey(@intCast(code), false, false);
|
||||||
};
|
};
|
||||||
Text.cancel(w);
|
Text.cancel(w);
|
||||||
}
|
}
|
||||||
s.key_focus = null;
|
s.key_focus = null;
|
||||||
s.menu_keys = @splat(false);
|
// Preserve consumed held keys until the next enter supplies its held-key set.
|
||||||
s.modifiers = 0;
|
s.modifiers = 0;
|
||||||
s.serial = 0;
|
s.serial = 0;
|
||||||
if (s.state != null) _ = c.z_xkb_state_update_mask.?(s.state, 0, 0, 0, 0, 0, 0);
|
if (s.state != null) _ = c.z_xkb_state_update_mask.?(s.state, 0, 0, 0, 0, 0, 0);
|
||||||
normal();
|
normal();
|
||||||
}
|
}
|
||||||
fn pointerEvent(kind: u32, button_: u32, pressed: bool, delta: n.uhdk_point) void {
|
fn pointerEvent(kind: u32, button_: u32, pressed: bool, delta: n.uhdk_point) void {
|
||||||
|
if (Popup.active()) return;
|
||||||
const s = state();
|
const s = state();
|
||||||
const w = s.pointer_focus orelse return;
|
const w = s.pointer_focus orelse return;
|
||||||
var e = std.mem.zeroes(n.z_event);
|
var e = std.mem.zeroes(n.z_event);
|
||||||
|
|
@ -327,6 +347,7 @@ fn pointerEnter(_: ?*anyopaque, _: ?*c.struct_wl_pointer, serial: u32, surface:
|
||||||
s.pointer_focus = find(surface);
|
s.pointer_focus = find(surface);
|
||||||
s.pointer_serial = serial;
|
s.pointer_serial = serial;
|
||||||
s.position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
s.position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
||||||
|
if (Popup.pointerEnter(surface, s.position.x, s.position.y, serial)) return;
|
||||||
pointerEvent(n.UHDK_EVENT_POINTER_ENTER, 0, false, .{ .x = 0, .y = 0 });
|
pointerEvent(n.UHDK_EVENT_POINTER_ENTER, 0, false, .{ .x = 0, .y = 0 });
|
||||||
if (s.pointer_focus) |w| _ = cursor(w, w.cursor);
|
if (s.pointer_focus) |w| _ = cursor(w, w.cursor);
|
||||||
}
|
}
|
||||||
|
|
@ -334,6 +355,7 @@ fn pointerLeave(_: ?*anyopaque, _: ?*c.struct_wl_pointer, _: u32, _: ?*c.struct_
|
||||||
leavePointer();
|
leavePointer();
|
||||||
}
|
}
|
||||||
fn leavePointer() void {
|
fn leavePointer() void {
|
||||||
|
Popup.pointerLeave();
|
||||||
const s = state();
|
const s = state();
|
||||||
normal();
|
normal();
|
||||||
pointerEvent(n.UHDK_EVENT_POINTER_LEAVE, 0, false, .{ .x = 0, .y = 0 });
|
pointerEvent(n.UHDK_EVENT_POINTER_LEAVE, 0, false, .{ .x = 0, .y = 0 });
|
||||||
|
|
@ -350,6 +372,10 @@ fn motion(_: ?*anyopaque, _: ?*c.struct_wl_pointer, _: u32, x: i32, y: i32) call
|
||||||
const s = state();
|
const s = state();
|
||||||
const previous = s.position;
|
const previous = s.position;
|
||||||
s.position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
s.position = .{ .x = @as(f32, @floatFromInt(x)) / 256, .y = @as(f32, @floatFromInt(y)) / 256 };
|
||||||
|
if (Popup.active()) {
|
||||||
|
Popup.motion(s.position.x, s.position.y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
pointerEvent(n.UHDK_EVENT_POINTER_MOVE, 0, false, .{ .x = s.position.x - previous.x, .y = s.position.y - previous.y });
|
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 {
|
fn button(_: ?*anyopaque, _: ?*c.struct_wl_pointer, serial: u32, _: u32, code: u32, pressed: u32) callconv(.c) void {
|
||||||
|
|
@ -365,6 +391,17 @@ fn button(_: ?*anyopaque, _: ?*c.struct_wl_pointer, serial: u32, _: u32, code: u
|
||||||
};
|
};
|
||||||
if (mapped == 0) return;
|
if (mapped == 0) return;
|
||||||
const mask = @as(u32, 1) << @intCast(mapped);
|
const mask = @as(u32, 1) << @intCast(mapped);
|
||||||
|
if (Popup.active()) {
|
||||||
|
if (pressed != 0) s.popup_buttons |= mask else s.popup_buttons &= ~mask;
|
||||||
|
Popup.button(code, pressed != 0, serial);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pressed == 0 and s.popup_buttons & mask != 0) {
|
||||||
|
s.popup_buttons &= ~mask;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pressed != 0) popupTrigger(s.pointer_focus, serial);
|
||||||
|
|
||||||
if (pressed != 0) {
|
if (pressed != 0) {
|
||||||
s.buttons |= mask;
|
s.buttons |= mask;
|
||||||
const now = runtime.now(b().app.io);
|
const now = runtime.now(b().app.io);
|
||||||
|
|
@ -402,6 +439,11 @@ fn frame(_: ?*anyopaque, _: ?*c.struct_wl_pointer) callconv(.c) void {
|
||||||
s.wheel = .{ .x = 0, .y = 0 };
|
s.wheel = .{ .x = 0, .y = 0 };
|
||||||
s.axis_stop = false;
|
s.axis_stop = false;
|
||||||
}
|
}
|
||||||
|
if (Popup.active()) {
|
||||||
|
const delta = if (s.wheel.y != 0) s.wheel.y * 26 else s.axis.y;
|
||||||
|
if (delta != 0) Popup.scroll(delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const w = s.pointer_focus orelse return;
|
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 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;
|
const has_wheel = s.wheel.x != 0 or s.wheel.y != 0;
|
||||||
|
|
@ -453,7 +495,25 @@ fn normal() void {
|
||||||
send(w, &e);
|
send(w, &e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn popupTrigger(w: ?*native.Window, serial: u32) void {
|
||||||
|
const s = state();
|
||||||
|
s.popup_window = w;
|
||||||
|
s.popup_serial = serial;
|
||||||
|
s.popup_time = runtime.now(b().app.io);
|
||||||
|
}
|
||||||
|
pub fn beginPopup() void {
|
||||||
|
normal();
|
||||||
|
state().buttons = 0;
|
||||||
|
state().scrolling = false;
|
||||||
|
}
|
||||||
|
pub fn clearPopupTrigger(w: *native.Window) void {
|
||||||
|
if (state().popup_window == w) {
|
||||||
|
state().popup_window = null;
|
||||||
|
state().popup_serial = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn forget(w: *native.Window) void {
|
pub fn forget(w: *native.Window) void {
|
||||||
|
clearPopupTrigger(w);
|
||||||
if (state().key_focus == w) leaveKeyboard();
|
if (state().key_focus == w) leaveKeyboard();
|
||||||
if (state().pointer_focus == w) leavePointer();
|
if (state().pointer_focus == w) leavePointer();
|
||||||
Text.forget(w);
|
Text.forget(w);
|
||||||
|
|
@ -462,6 +522,9 @@ pub fn remove(name: u32) void {
|
||||||
if (name == state().seat_name) releaseSeat();
|
if (name == state().seat_name) releaseSeat();
|
||||||
}
|
}
|
||||||
fn releaseSeat() void {
|
fn releaseSeat() void {
|
||||||
|
Popup.deinit();
|
||||||
|
state().popup_serial = 0;
|
||||||
|
state().popup_window = null;
|
||||||
Data.releaseSeat();
|
Data.releaseSeat();
|
||||||
const s = state();
|
const s = state();
|
||||||
if (s.key_sync != null) c.wl_callback_destroy(s.key_sync);
|
if (s.key_sync != null) c.wl_callback_destroy(s.key_sync);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const Resource = @import("../Resource.zig");
|
||||||
const abi = @import("../abi.zig");
|
const abi = @import("../abi.zig");
|
||||||
const Bus = @import("Bus.zig");
|
const Bus = @import("Bus.zig");
|
||||||
const Portal = @import("Portal.zig");
|
const Portal = @import("Portal.zig");
|
||||||
|
const Menu = @import("Menu.zig");
|
||||||
const Input = @import("Input.zig");
|
const Input = @import("Input.zig");
|
||||||
const iface = "com.canonical.dbusmenu";
|
const iface = "com.canonical.dbusmenu";
|
||||||
const prefix = "/org/uhdk/menu/";
|
const prefix = "/org/uhdk/menu/";
|
||||||
|
|
@ -195,21 +196,7 @@ fn enabled(e: *Export, index: usize) bool {
|
||||||
fn activate(e: *Export, index: usize, input_id: u64) void {
|
fn activate(e: *Export, index: usize, input_id: u64) void {
|
||||||
const item = e.nodes.items[index].item orelse return;
|
const item = e.nodes.items[index].item orelse return;
|
||||||
if (!enabled(e, index) or item.flags & n.UHDK_MENU_SEPARATOR != 0 or item.submenu != null) return;
|
if (!enabled(e, index) or item.flags & n.UHDK_MENU_SEPARATOR != 0 or item.submenu != null) return;
|
||||||
var event = std.mem.zeroes(n.z_event);
|
Menu.activate(e.window, item, input_id);
|
||||||
if (item.role == n.UHDK_MENU_ROLE_QUIT) {
|
|
||||||
event.type = n.UHDK_EVENT_QUIT_REQUESTED;
|
|
||||||
n.z_emit(runtime.toAppHandle(b().app), &event);
|
|
||||||
} else if (item.role >= n.UHDK_MENU_ROLE_COPY and item.role <= n.UHDK_MENU_ROLE_REDO) {
|
|
||||||
event.type = n.UHDK_EVENT_EDIT_COMMAND;
|
|
||||||
event.input_id = input_id;
|
|
||||||
event.data.edit.command = @as(u32, n.UHDK_EDIT_COPY) + item.role - n.UHDK_MENU_ROLE_COPY;
|
|
||||||
e.window.text.last_edit = input_id;
|
|
||||||
Input.send(e.window, &event);
|
|
||||||
} else {
|
|
||||||
event.type = n.UHDK_EVENT_MENU_COMMAND;
|
|
||||||
event.data.menu.command_id = item.command_id;
|
|
||||||
Input.send(e.window, &event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fn folded(key: u32) u32 {
|
fn folded(key: u32) u32 {
|
||||||
return if (key >= 'A' and key <= 'Z') key + ('a' - 'A') else key;
|
return if (key >= 'A' and key <= 'Z') key + ('a' - 'A') else key;
|
||||||
|
|
@ -237,32 +224,6 @@ fn variant(i: *Bus.Iter, comptime sig: [:0]const u8, value: anytype) !void {
|
||||||
try Bus.basic(&v, sig[0], value);
|
try Bus.basic(&v, sig[0], value);
|
||||||
try Bus.close(i, &v);
|
try Bus.close(i, &v);
|
||||||
}
|
}
|
||||||
fn keyName(key: u32, buffer: *[32]u8) ?[:0]const u8 {
|
|
||||||
if (key >= n.UHDK_KEY_F1 and key < n.UHDK_KEY_F1 + 24) return std.fmt.bufPrintZ(buffer, "F{d}", .{key - n.UHDK_KEY_F1 + 1}) catch unreachable;
|
|
||||||
switch (key) {
|
|
||||||
n.UHDK_KEY_ESCAPE => return "Escape",
|
|
||||||
n.UHDK_KEY_ENTER => return "Return",
|
|
||||||
n.UHDK_KEY_TAB => return "Tab",
|
|
||||||
n.UHDK_KEY_BACKSPACE => return "BackSpace",
|
|
||||||
n.UHDK_KEY_DELETE => return "Delete",
|
|
||||||
n.UHDK_KEY_INSERT => return "Insert",
|
|
||||||
n.UHDK_KEY_LEFT => return "Left",
|
|
||||||
n.UHDK_KEY_RIGHT => return "Right",
|
|
||||||
n.UHDK_KEY_UP => return "Up",
|
|
||||||
n.UHDK_KEY_DOWN => return "Down",
|
|
||||||
n.UHDK_KEY_HOME => return "Home",
|
|
||||||
n.UHDK_KEY_END => return "End",
|
|
||||||
n.UHDK_KEY_PAGE_UP => return "Page_Up",
|
|
||||||
n.UHDK_KEY_PAGE_DOWN => return "Page_Down",
|
|
||||||
' ' => return "space",
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
if (key < 0x21 or key > 0x10ffff) return null;
|
|
||||||
const k = if (key >= 'a' and key <= 'z') key - ('a' - 'A') else key;
|
|
||||||
const len = std.unicode.utf8Encode(@intCast(k), buffer) catch return null;
|
|
||||||
buffer[len] = 0;
|
|
||||||
return buffer[0..len :0];
|
|
||||||
}
|
|
||||||
fn property(e: *Export, index: usize, name: []const u8, out: *Bus.Iter) !bool {
|
fn property(e: *Export, index: usize, name: []const u8, out: *Bus.Iter) !bool {
|
||||||
const node = e.nodes.items[index];
|
const node = e.nodes.items[index];
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
|
|
@ -273,7 +234,7 @@ fn property(e: *Export, index: usize, name: []const u8, out: *Bus.Iter) !bool {
|
||||||
var arrays = try Bus.open(&v, c.DBUS_TYPE_ARRAY, "as");
|
var arrays = try Bus.open(&v, c.DBUS_TYPE_ARRAY, "as");
|
||||||
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&v, &arrays);
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&v, &arrays);
|
||||||
var buffer: [32]u8 = undefined;
|
var buffer: [32]u8 = undefined;
|
||||||
if (item) |value| if (keyName(value.shortcut_key, &buffer)) |key_name| {
|
if (item) |value| if (Menu.keyName(value.shortcut_key, &buffer)) |key_name| {
|
||||||
var keys = try Bus.open(&arrays, c.DBUS_TYPE_ARRAY, "s");
|
var keys = try Bus.open(&arrays, c.DBUS_TYPE_ARRAY, "s");
|
||||||
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&arrays, &keys);
|
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&arrays, &keys);
|
||||||
inline for (.{ .{ n.UHDK_MOD_CONTROL, "Control" }, .{ n.UHDK_MOD_ALT, "Alt" }, .{ n.UHDK_MOD_SHIFT, "Shift" }, .{ n.UHDK_MOD_SUPER, "Super" } }) |mod| {
|
inline for (.{ .{ n.UHDK_MOD_CONTROL, "Control" }, .{ n.UHDK_MOD_ALT, "Alt" }, .{ n.UHDK_MOD_SHIFT, "Shift" }, .{ n.UHDK_MOD_SUPER, "Super" } }) |mod| {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
//! Linux menu role dispatch and shortcut names shared by desktop exports/popups.
|
||||||
|
const std = @import("std");
|
||||||
|
const n = @import("native");
|
||||||
|
const native = @import("native.zig");
|
||||||
|
const runtime = @import("../runtime.zig");
|
||||||
|
const Input = @import("Input.zig");
|
||||||
|
pub fn activate(window: *native.Window, item: *const n.uhdk_menu_item, input_id: u64) void {
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
if (item.role == n.UHDK_MENU_ROLE_QUIT) {
|
||||||
|
event.type = n.UHDK_EVENT_QUIT_REQUESTED;
|
||||||
|
n.z_emit(runtime.toAppHandle(window.owner), &event);
|
||||||
|
} else if (item.role >= n.UHDK_MENU_ROLE_COPY and item.role <= n.UHDK_MENU_ROLE_REDO) {
|
||||||
|
event.type = n.UHDK_EVENT_EDIT_COMMAND;
|
||||||
|
event.input_id = input_id;
|
||||||
|
event.data.edit.command = @as(u32, n.UHDK_EDIT_COPY) + item.role - n.UHDK_MENU_ROLE_COPY;
|
||||||
|
window.text.last_edit = input_id;
|
||||||
|
Input.send(window, &event);
|
||||||
|
} else {
|
||||||
|
event.type = n.UHDK_EVENT_MENU_COMMAND;
|
||||||
|
event.data.menu.command_id = item.command_id;
|
||||||
|
Input.send(window, &event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn keyName(key: u32, buffer: *[32]u8) ?[:0]const u8 {
|
||||||
|
if (key >= n.UHDK_KEY_F1 and key < n.UHDK_KEY_F1 + 24) return std.fmt.bufPrintZ(buffer, "F{d}", .{key - n.UHDK_KEY_F1 + 1}) catch unreachable;
|
||||||
|
switch (key) {
|
||||||
|
n.UHDK_KEY_ESCAPE => return "Escape",
|
||||||
|
n.UHDK_KEY_ENTER => return "Return",
|
||||||
|
n.UHDK_KEY_TAB => return "Tab",
|
||||||
|
n.UHDK_KEY_BACKSPACE => return "BackSpace",
|
||||||
|
n.UHDK_KEY_DELETE => return "Delete",
|
||||||
|
n.UHDK_KEY_INSERT => return "Insert",
|
||||||
|
n.UHDK_KEY_LEFT => return "Left",
|
||||||
|
n.UHDK_KEY_RIGHT => return "Right",
|
||||||
|
n.UHDK_KEY_UP => return "Up",
|
||||||
|
n.UHDK_KEY_DOWN => return "Down",
|
||||||
|
n.UHDK_KEY_HOME => return "Home",
|
||||||
|
n.UHDK_KEY_END => return "End",
|
||||||
|
n.UHDK_KEY_PAGE_UP => return "Page_Up",
|
||||||
|
n.UHDK_KEY_PAGE_DOWN => return "Page_Down",
|
||||||
|
' ' => return "space",
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
if (key < 0x21 or key > 0x10ffff) return null;
|
||||||
|
const k = if (key >= 'a' and key <= 'z') key - ('a' - 'A') else key;
|
||||||
|
const len = std.unicode.utf8Encode(@intCast(k), buffer) catch return null;
|
||||||
|
buffer[len] = 0;
|
||||||
|
return buffer[0..len :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shortcut(item: n.uhdk_menu_item, buffer: *[96]u8) []const u8 {
|
||||||
|
var key_buffer: [32]u8 = undefined;
|
||||||
|
const key = keyName(item.shortcut_key, &key_buffer) orelse return "";
|
||||||
|
var end: usize = 0;
|
||||||
|
inline for (.{ .{ n.UHDK_MOD_CONTROL, "Ctrl+" }, .{ n.UHDK_MOD_ALT, "Alt+" }, .{ n.UHDK_MOD_SHIFT, "Shift+" }, .{ n.UHDK_MOD_SUPER, "Super+" } }) |mod| {
|
||||||
|
if (item.shortcut_modifiers & mod[0] != 0) {
|
||||||
|
@memcpy(buffer[end..][0..mod[1].len], mod[1]);
|
||||||
|
end += mod[1].len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@memcpy(buffer[end..][0..key.len], key);
|
||||||
|
end += key.len;
|
||||||
|
return buffer[0..end];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,464 @@
|
||||||
|
//! Small software-rendered xdg_popup menus. One active request owns a stack of
|
||||||
|
//! panels, with two wl_shm buffers per panel. Everything runs on 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 Resource = @import("../Resource.zig");
|
||||||
|
const abi = @import("../abi.zig");
|
||||||
|
const Input = @import("Input.zig");
|
||||||
|
const Menu = @import("Menu.zig");
|
||||||
|
const Bitmap = @import("Bitmap.zig");
|
||||||
|
const Buffer = struct { proxy: ?*c.struct_wl_buffer = null, mapping: ?*anyopaque = null, size: usize = 0, busy: bool = false };
|
||||||
|
const Panel = struct {
|
||||||
|
parent: ?*Panel,
|
||||||
|
menu: *Resource,
|
||||||
|
surface: ?*c.struct_wl_surface = null,
|
||||||
|
xdg: ?*c.struct_xdg_surface = null,
|
||||||
|
popup: ?*c.struct_xdg_popup = null,
|
||||||
|
buffers: [2]Buffer = .{ .{}, .{} },
|
||||||
|
width: i32 = 180,
|
||||||
|
height: i32 = 32,
|
||||||
|
scale: i32 = 1,
|
||||||
|
pending_width: i32 = 180,
|
||||||
|
pending_height: i32 = 32,
|
||||||
|
total: i32 = 8,
|
||||||
|
scroll: i32 = 0,
|
||||||
|
selected: ?usize = null,
|
||||||
|
pressed: ?usize = null,
|
||||||
|
configured: bool = false,
|
||||||
|
dirty: bool = true,
|
||||||
|
deadline: u64 = 0,
|
||||||
|
};
|
||||||
|
pub const State = struct {
|
||||||
|
window: ?*native.Window = null,
|
||||||
|
root: ?*Resource = null,
|
||||||
|
top: ?*Panel = null,
|
||||||
|
pointer: ?*Panel = null,
|
||||||
|
request: u64 = 0,
|
||||||
|
serial: u32 = 0,
|
||||||
|
hover_at: u64 = 0,
|
||||||
|
};
|
||||||
|
fn b() *native.Backend {
|
||||||
|
return native.instance.?;
|
||||||
|
}
|
||||||
|
fn s() *State {
|
||||||
|
return &b().popup;
|
||||||
|
}
|
||||||
|
pub fn active() bool {
|
||||||
|
return s().request != 0;
|
||||||
|
}
|
||||||
|
pub fn capability() void {
|
||||||
|
native.setCapability(n.UHDK_CAP_CONTEXT_MENU, b().desktop.shm != null and Input.state().seat != null);
|
||||||
|
}
|
||||||
|
fn panel(raw: ?*anyopaque) *Panel {
|
||||||
|
return @ptrCast(@alignCast(raw.?));
|
||||||
|
}
|
||||||
|
fn rowHeight(item: n.uhdk_menu_item) i32 {
|
||||||
|
return if (item.flags & n.UHDK_MENU_SEPARATOR != 0) 9 else 26;
|
||||||
|
}
|
||||||
|
fn usable(item: n.uhdk_menu_item) bool {
|
||||||
|
return item.flags & (n.UHDK_MENU_DISABLED | n.UHDK_MENU_SEPARATOR) == 0;
|
||||||
|
}
|
||||||
|
fn rowY(p: *Panel, index: usize) i32 {
|
||||||
|
var y: i32 = 4;
|
||||||
|
for (p.menu.payload.menu.items[0..index]) |item| y += rowHeight(item);
|
||||||
|
return y - p.scroll;
|
||||||
|
}
|
||||||
|
fn lookup(surface: ?*c.struct_wl_surface) ?*Panel {
|
||||||
|
var cursor = s().top;
|
||||||
|
while (cursor) |p| : (cursor = p.parent) if (p.surface == surface) return p;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
pub fn owner(surface: ?*c.struct_wl_surface) ?*native.Window {
|
||||||
|
return if (lookup(surface) != null) s().window else null;
|
||||||
|
}
|
||||||
|
fn freeBuffer(buf: *Buffer) void {
|
||||||
|
if (buf.proxy != null) c.wl_buffer_destroy(buf.proxy);
|
||||||
|
if (buf.mapping) |mapping| _ = c.munmap(mapping, buf.size);
|
||||||
|
buf.* = .{};
|
||||||
|
}
|
||||||
|
fn destroy(p: *Panel) void {
|
||||||
|
if (s().pointer == p) s().pointer = null;
|
||||||
|
if (p.popup != null) c.xdg_popup_destroy(p.popup);
|
||||||
|
if (p.xdg != null) c.xdg_surface_destroy(p.xdg);
|
||||||
|
if (p.surface != null) c.wl_surface_destroy(p.surface);
|
||||||
|
for (&p.buffers) |*buf| freeBuffer(buf);
|
||||||
|
b().app.gpa.destroy(p);
|
||||||
|
}
|
||||||
|
fn closeChildren(p: *Panel) void {
|
||||||
|
while (s().top != p) {
|
||||||
|
const top = s().top orelse return;
|
||||||
|
s().top = top.parent;
|
||||||
|
destroy(top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn finish(status: u32) void {
|
||||||
|
if (!active()) return;
|
||||||
|
const request_id = s().request;
|
||||||
|
while (s().top) |top| {
|
||||||
|
s().top = top.parent;
|
||||||
|
destroy(top);
|
||||||
|
}
|
||||||
|
const root = s().root.?;
|
||||||
|
s().* = .{};
|
||||||
|
runtime.release(root);
|
||||||
|
n.z_complete(runtime.toAppHandle(b().app), request_id, status, null);
|
||||||
|
}
|
||||||
|
pub fn forget(w: *native.Window) void {
|
||||||
|
if (s().window == w) finish(abi.CANCELLED);
|
||||||
|
}
|
||||||
|
pub fn cancel(id: u64) bool {
|
||||||
|
if (s().request != id or !active()) return false;
|
||||||
|
finish(abi.CANCELLED);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pub fn deinit() void {
|
||||||
|
finish(abi.CANCELLED);
|
||||||
|
}
|
||||||
|
pub fn request(cmd: *const n.z_command, id: u64) u32 {
|
||||||
|
if (active()) return n.UHDK_STATUS_BUSY;
|
||||||
|
if (b().desktop.shm == null or Input.state().seat == null) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
const w: *native.Window = @ptrCast(@alignCast(n.z_object_native(cmd.other) orelse return abi.UNAVAILABLE));
|
||||||
|
const input = Input.state();
|
||||||
|
if (!w.visible or !w.configured or input.popup_window != w or input.popup_serial == 0 or runtime.now(b().app.io) -| input.popup_time > 5 * std.time.ns_per_s) return abi.UNAVAILABLE;
|
||||||
|
const root: *Resource = @ptrCast(@alignCast(cmd.target.?));
|
||||||
|
if (root.payload.menu.items.len == 0) return abi.CANCELLED;
|
||||||
|
s().window = w;
|
||||||
|
s().root = root;
|
||||||
|
s().serial = input.popup_serial;
|
||||||
|
runtime.retain(root);
|
||||||
|
const point: n.uhdk_point = .{ .x = std.math.clamp(cmd.point.x, 0, @as(f32, @floatFromInt(@max(0, w.width - 1)))), .y = std.math.clamp(cmd.point.y, 0, @as(f32, @floatFromInt(@max(0, w.height - 1)))) };
|
||||||
|
const p = create(root, null, @intFromFloat(point.x), @intFromFloat(point.y), 1) catch {
|
||||||
|
runtime.release(root);
|
||||||
|
s().* = .{};
|
||||||
|
return abi.OOM;
|
||||||
|
};
|
||||||
|
s().top = p;
|
||||||
|
s().request = id;
|
||||||
|
Input.beginPopup();
|
||||||
|
input.popup_serial = 0; // A root grab consumes its triggering action.
|
||||||
|
return n.Z_DEFERRED;
|
||||||
|
}
|
||||||
|
fn create(menu: *Resource, parent: ?*Panel, x: i32, y: i32, anchor_height: i32) !*Panel {
|
||||||
|
const p = try b().app.gpa.create(Panel);
|
||||||
|
p.* = .{ .parent = parent, .menu = menu, .scale = @intCast(std.math.clamp((s().window.?.scale + 119) / 120, 1, 4)), .deadline = runtime.now(b().app.io) + 5 * std.time.ns_per_s };
|
||||||
|
errdefer destroy(p);
|
||||||
|
for (menu.payload.menu.items) |item| {
|
||||||
|
var buffer: [96]u8 = undefined;
|
||||||
|
p.width = @max(p.width, 60 + Bitmap.width(abi.textBytes(item.label)) + Bitmap.width(Menu.shortcut(item, &buffer)));
|
||||||
|
p.total += rowHeight(item);
|
||||||
|
}
|
||||||
|
p.width = @min(p.width, 480);
|
||||||
|
p.height = @min(p.total, 480);
|
||||||
|
p.pending_width = p.width;
|
||||||
|
p.pending_height = p.height;
|
||||||
|
p.surface = c.wl_compositor_create_surface(b().compositor) orelse return error.OutOfMemory;
|
||||||
|
if (c.wl_surface_add_listener(p.surface, &surface_listener, p) != 0) return error.OutOfMemory;
|
||||||
|
p.xdg = c.xdg_wm_base_get_xdg_surface(b().shell, p.surface) orelse return error.OutOfMemory;
|
||||||
|
if (c.xdg_surface_add_listener(p.xdg, &xdg_listener, p) != 0) return error.OutOfMemory;
|
||||||
|
const pos = c.xdg_wm_base_create_positioner(b().shell) orelse return error.OutOfMemory;
|
||||||
|
defer c.xdg_positioner_destroy(pos);
|
||||||
|
c.xdg_positioner_set_size(pos, p.width, p.height);
|
||||||
|
c.xdg_positioner_set_anchor_rect(pos, x, y, 1, @max(1, anchor_height));
|
||||||
|
c.xdg_positioner_set_anchor(pos, if (parent != null) c.XDG_POSITIONER_ANCHOR_TOP_RIGHT else c.XDG_POSITIONER_ANCHOR_BOTTOM_LEFT);
|
||||||
|
c.xdg_positioner_set_gravity(pos, c.XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
|
||||||
|
c.xdg_positioner_set_constraint_adjustment(pos, c.XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | c.XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | c.XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y | c.XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | c.XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y);
|
||||||
|
p.popup = c.xdg_surface_get_popup(p.xdg, if (parent) |v| v.xdg else s().window.?.xdg, pos) orelse return error.OutOfMemory;
|
||||||
|
if (c.xdg_popup_add_listener(p.popup, &popup_listener, p) != 0) return error.OutOfMemory;
|
||||||
|
c.xdg_popup_grab(p.popup, Input.state().seat, s().serial);
|
||||||
|
c.wl_surface_commit(p.surface);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
fn configure(raw: ?*anyopaque, _: ?*c.struct_xdg_popup, _: i32, _: i32, width: i32, height: i32) callconv(.c) void {
|
||||||
|
const p = panel(raw);
|
||||||
|
if (width < 1 or height < 1 or width > 1024 or height > 1024) {
|
||||||
|
finish(n.UHDK_STATUS_OS_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Geometry belongs to the next xdg_surface.configure, which can arrive in
|
||||||
|
// another socket read. Do not render/commit it before acknowledging that.
|
||||||
|
p.pending_width = width;
|
||||||
|
p.pending_height = height;
|
||||||
|
p.configured = false;
|
||||||
|
p.deadline = runtime.now(b().app.io) + 5 * std.time.ns_per_s;
|
||||||
|
}
|
||||||
|
fn configured(raw: ?*anyopaque, _: ?*c.struct_xdg_surface, serial: u32) callconv(.c) void {
|
||||||
|
const p = panel(raw);
|
||||||
|
c.xdg_surface_ack_configure(p.xdg, serial);
|
||||||
|
if (p.width != p.pending_width or p.height != p.pending_height) {
|
||||||
|
for (&p.buffers) |*buf| freeBuffer(buf);
|
||||||
|
p.width = p.pending_width;
|
||||||
|
p.height = p.pending_height;
|
||||||
|
p.scroll = std.math.clamp(p.scroll, 0, @max(0, p.total - p.height));
|
||||||
|
}
|
||||||
|
p.configured = true;
|
||||||
|
p.deadline = 0;
|
||||||
|
p.dirty = true;
|
||||||
|
render(p) catch finish(abi.OOM);
|
||||||
|
}
|
||||||
|
fn done(raw: ?*anyopaque, _: ?*c.struct_xdg_popup) callconv(.c) void {
|
||||||
|
const p = panel(raw);
|
||||||
|
if (p.parent) |parent| {
|
||||||
|
closeChildren(parent);
|
||||||
|
s().hover_at = 0;
|
||||||
|
} else finish(abi.CANCELLED);
|
||||||
|
}
|
||||||
|
fn repositioned(_: ?*anyopaque, _: ?*c.struct_xdg_popup, _: u32) callconv(.c) void {}
|
||||||
|
const popup_listener: c.struct_xdg_popup_listener = .{ .configure = configure, .popup_done = done, .repositioned = repositioned };
|
||||||
|
const xdg_listener: c.struct_xdg_surface_listener = .{ .configure = configured };
|
||||||
|
fn enter(_: ?*anyopaque, _: ?*c.struct_wl_surface, _: ?*c.struct_wl_output) callconv(.c) void {}
|
||||||
|
fn scale(raw: ?*anyopaque, _: ?*c.struct_wl_surface, value: i32) callconv(.c) void {
|
||||||
|
const p = panel(raw);
|
||||||
|
const next = std.math.clamp(value, 1, 4);
|
||||||
|
if (next == p.scale) return;
|
||||||
|
p.scale = next;
|
||||||
|
for (&p.buffers) |*buf| freeBuffer(buf);
|
||||||
|
p.dirty = true;
|
||||||
|
}
|
||||||
|
fn transform(_: ?*anyopaque, _: ?*c.struct_wl_surface, _: u32) callconv(.c) void {}
|
||||||
|
const surface_listener: c.struct_wl_surface_listener = .{ .enter = enter, .leave = enter, .preferred_buffer_scale = scale, .preferred_buffer_transform = transform };
|
||||||
|
fn released(raw: ?*anyopaque, proxy: ?*c.struct_wl_buffer) callconv(.c) void {
|
||||||
|
const p = panel(raw);
|
||||||
|
for (&p.buffers) |*buf| if (buf.proxy == proxy) {
|
||||||
|
buf.busy = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const buffer_listener: c.struct_wl_buffer_listener = .{ .release = released };
|
||||||
|
fn allocateBuffer(p: *Panel, buf: *Buffer) !void {
|
||||||
|
const size: usize = @intCast(p.width * p.height * p.scale * p.scale * 4);
|
||||||
|
const fd = c.memfd_create("uhdk-menu", c.MFD_CLOEXEC);
|
||||||
|
if (fd < 0) return error.OutOfMemory;
|
||||||
|
defer _ = c.close(fd);
|
||||||
|
if (c.ftruncate(fd, @intCast(size)) != 0) return error.OutOfMemory;
|
||||||
|
const mapping = c.mmap(null, size, c.PROT_READ | c.PROT_WRITE, c.MAP_SHARED, fd, 0);
|
||||||
|
if (mapping == c.MAP_FAILED) return error.OutOfMemory;
|
||||||
|
buf.mapping = mapping;
|
||||||
|
buf.size = size;
|
||||||
|
errdefer freeBuffer(buf);
|
||||||
|
const pool = c.wl_shm_create_pool(b().desktop.shm, fd, @intCast(size)) orelse return error.OutOfMemory;
|
||||||
|
defer c.wl_shm_pool_destroy(pool);
|
||||||
|
buf.proxy = c.wl_shm_pool_create_buffer(pool, 0, p.width * p.scale, p.height * p.scale, p.width * p.scale * 4, c.WL_SHM_FORMAT_ARGB8888) orelse return error.OutOfMemory;
|
||||||
|
if (c.wl_buffer_add_listener(buf.proxy, &buffer_listener, p) != 0) return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
fn render(p: *Panel) !void {
|
||||||
|
if (!p.configured or !p.dirty) return;
|
||||||
|
const buf: *Buffer = for (&p.buffers) |*value| {
|
||||||
|
if (!value.busy) break value;
|
||||||
|
} else return;
|
||||||
|
if (buf.proxy == null) try allocateBuffer(p, buf);
|
||||||
|
const words: [*]u32 = @ptrCast(@alignCast(buf.mapping.?));
|
||||||
|
const canvas: Bitmap.Canvas = .{ .pixels = words[0 .. buf.size / 4], .width = p.width, .height = p.height, .scale = p.scale };
|
||||||
|
paint(canvas, p.menu.payload.menu.items, p.selected, p.scroll, b().portal.preferences.appearance == n.UHDK_APPEARANCE_LIGHT);
|
||||||
|
c.wl_surface_set_buffer_scale(p.surface, p.scale);
|
||||||
|
c.xdg_surface_set_window_geometry(p.xdg, 0, 0, p.width, p.height);
|
||||||
|
c.wl_surface_attach(p.surface, buf.proxy, 0, 0);
|
||||||
|
c.wl_surface_damage_buffer(p.surface, 0, 0, p.width * p.scale, p.height * p.scale);
|
||||||
|
c.wl_surface_commit(p.surface);
|
||||||
|
buf.busy = true;
|
||||||
|
p.dirty = false;
|
||||||
|
}
|
||||||
|
pub fn paint(canvas: Bitmap.Canvas, items: []const n.uhdk_menu_item, selected: ?usize, scroll_offset: i32, light: bool) void {
|
||||||
|
const background: u32 = if (light) 0xf6f6f6 else 0x292c30;
|
||||||
|
const foreground: u32 = if (light) 0x202124 else 0xf1f2f3;
|
||||||
|
const border: u32 = if (light) 0xc4c7ca else 0x53585e;
|
||||||
|
canvas.rect(0, 0, canvas.width, canvas.height, background);
|
||||||
|
var y: i32 = 4 - scroll_offset;
|
||||||
|
for (items, 0..) |item, i| {
|
||||||
|
const height = rowHeight(item);
|
||||||
|
defer y += height;
|
||||||
|
if (y + height <= 4 or y >= canvas.height - 4) continue;
|
||||||
|
if (item.flags & n.UHDK_MENU_SEPARATOR != 0) {
|
||||||
|
canvas.rect(8, y + 4, canvas.width - 16, 1, border);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const color: u32 = if (item.flags & n.UHDK_MENU_DISABLED != 0) (if (light) @as(u32, 0x85888c) else 0x8d939a) else if (selected == i) 0xffffff else foreground;
|
||||||
|
if (selected == i and usable(item)) canvas.rect(3, y, canvas.width - 6, height, 0x2969b0);
|
||||||
|
if (item.flags & n.UHDK_MENU_CHECKED != 0) {
|
||||||
|
for (0..4) |j| canvas.rect(10 + @as(i32, @intCast(j)), y + 12 + @as(i32, @intCast(j)), 2, 2, color);
|
||||||
|
for (0..7) |j| canvas.rect(13 + @as(i32, @intCast(j)), y + 15 - @as(i32, @intCast(j)), 2, 2, color);
|
||||||
|
}
|
||||||
|
var storage: [96]u8 = undefined;
|
||||||
|
const shortcut = Menu.shortcut(item, &storage);
|
||||||
|
const shortcut_width = @min(Bitmap.width(shortcut), @max(0, @divTrunc(canvas.width, 2) - 30));
|
||||||
|
const label_width = @max(0, canvas.width - 52 - (if (shortcut.len != 0) shortcut_width + 18 else 0));
|
||||||
|
canvas.text(abi.textBytes(item.label), 28, y + 18, label_width, color);
|
||||||
|
if (shortcut.len != 0) canvas.text(shortcut, canvas.width - 22 - shortcut_width, y + 18, shortcut_width, color);
|
||||||
|
if (item.submenu != null) for (0..5) |j| {
|
||||||
|
const offset: i32 = @intCast(j);
|
||||||
|
canvas.rect(canvas.width - 13 + offset, y + 8 + offset, 1, 9 - 2 * offset, color);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Padding masks partially scrolled rows and keeps the border intact.
|
||||||
|
canvas.rect(0, 0, canvas.width, 4, background);
|
||||||
|
canvas.rect(0, canvas.height - 4, canvas.width, 4, background);
|
||||||
|
canvas.rect(0, 0, canvas.width, 1, border);
|
||||||
|
canvas.rect(0, canvas.height - 1, canvas.width, 1, border);
|
||||||
|
canvas.rect(0, 0, 1, canvas.height, border);
|
||||||
|
canvas.rect(canvas.width - 1, 0, 1, canvas.height, border);
|
||||||
|
if (scroll_offset > 0) canvas.rect(@divTrunc(canvas.width, 2) - 5, 1, 10, 2, foreground);
|
||||||
|
if (y > canvas.height - 4) canvas.rect(@divTrunc(canvas.width, 2) - 5, canvas.height - 3, 10, 2, foreground);
|
||||||
|
}
|
||||||
|
fn choose(p: *Panel, index: ?usize) void {
|
||||||
|
if (p.selected == index) return;
|
||||||
|
closeChildren(p);
|
||||||
|
p.selected = index;
|
||||||
|
p.pressed = null;
|
||||||
|
p.dirty = true;
|
||||||
|
s().hover_at = if (index != null and p.menu.payload.menu.items[index.?].submenu != null) runtime.now(b().app.io) + 200 * std.time.ns_per_ms else 0;
|
||||||
|
}
|
||||||
|
fn hit(p: *Panel, x: f32, y: f32) ?usize {
|
||||||
|
if (x < 3 or x >= @as(f32, @floatFromInt(p.width - 3)) or y < 4 or y >= @as(f32, @floatFromInt(p.height - 4))) return null;
|
||||||
|
var top: i32 = 4 - p.scroll;
|
||||||
|
for (p.menu.payload.menu.items, 0..) |item, i| {
|
||||||
|
const bottom = top + rowHeight(item);
|
||||||
|
if (y >= @as(f32, @floatFromInt(top)) and y < @as(f32, @floatFromInt(bottom))) return if (usable(item)) i else null;
|
||||||
|
top = bottom;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
pub fn pointerEnter(surface: ?*c.struct_wl_surface, x: f32, y: f32, serial: u32) bool {
|
||||||
|
s().pointer = lookup(surface);
|
||||||
|
const p = s().pointer orelse return false;
|
||||||
|
const input = Input.state();
|
||||||
|
if (input.cursor_device != null) c.wp_cursor_shape_device_v1_set_shape(input.cursor_device, serial, c.WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
|
||||||
|
choose(p, hit(p, x, y));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pub fn pointerLeave() void {
|
||||||
|
s().pointer = null;
|
||||||
|
s().hover_at = 0;
|
||||||
|
}
|
||||||
|
pub fn motion(x: f32, y: f32) void {
|
||||||
|
if (s().pointer) |p| choose(p, hit(p, x, y));
|
||||||
|
}
|
||||||
|
pub fn button(code: u32, down: bool, serial: u32) void {
|
||||||
|
if (down) s().serial = serial;
|
||||||
|
const p = s().pointer orelse {
|
||||||
|
if (down) finish(abi.CANCELLED);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (code != 0x110) return;
|
||||||
|
if (down) p.pressed = p.selected else if (p.pressed != null and p.pressed == p.selected) select(p);
|
||||||
|
}
|
||||||
|
fn openChild(p: *Panel) void {
|
||||||
|
s().hover_at = 0;
|
||||||
|
const index = p.selected orelse return;
|
||||||
|
const item = p.menu.payload.menu.items[index];
|
||||||
|
if (!usable(item) or item.submenu == null) return;
|
||||||
|
closeChildren(p);
|
||||||
|
const menu: *Resource = @ptrCast(@alignCast(item.submenu.?));
|
||||||
|
if (menu.payload.menu.items.len == 0) return;
|
||||||
|
const y = std.math.clamp(rowY(p, index), 0, p.height - 1);
|
||||||
|
const child = create(menu, p, p.width - 1, y, @min(26, p.height - y)) catch {
|
||||||
|
finish(abi.OOM);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
s().top = child;
|
||||||
|
}
|
||||||
|
fn select(p: *Panel) void {
|
||||||
|
const index = p.selected orelse return;
|
||||||
|
const item = &p.menu.payload.menu.items[index];
|
||||||
|
if (!usable(item.*)) return;
|
||||||
|
if (item.submenu != null) {
|
||||||
|
openChild(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Menu.activate(s().window.?, item, Input.id());
|
||||||
|
finish(abi.OK); // Command event must precede request completion.
|
||||||
|
}
|
||||||
|
fn reveal(p: *Panel) void {
|
||||||
|
const index = p.selected orelse return;
|
||||||
|
const y = rowY(p, index);
|
||||||
|
if (y < 4) p.scroll -= 4 - y;
|
||||||
|
if (y + 26 > p.height - 4) p.scroll += y + 26 - (p.height - 4);
|
||||||
|
p.scroll = std.math.clamp(p.scroll, 0, @max(0, p.total - p.height));
|
||||||
|
p.dirty = true;
|
||||||
|
}
|
||||||
|
fn move(p: *Panel, forward: bool) void {
|
||||||
|
const items = p.menu.payload.menu.items;
|
||||||
|
if (items.len == 0) return;
|
||||||
|
var index = p.selected orelse (if (forward) items.len - 1 else 0);
|
||||||
|
for (0..items.len) |_| {
|
||||||
|
index = if (forward) (index + 1) % items.len else (index + items.len - 1) % items.len;
|
||||||
|
if (usable(items[index])) {
|
||||||
|
choose(p, index);
|
||||||
|
s().hover_at = 0;
|
||||||
|
reveal(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn key(logical: u32, down: bool, serial: u32) void {
|
||||||
|
if (!down) return;
|
||||||
|
const p = s().top orelse return;
|
||||||
|
s().serial = serial;
|
||||||
|
switch (logical) {
|
||||||
|
n.UHDK_KEY_ESCAPE => finish(abi.CANCELLED),
|
||||||
|
n.UHDK_KEY_UP => move(p, false),
|
||||||
|
n.UHDK_KEY_DOWN, n.UHDK_KEY_TAB => move(p, true),
|
||||||
|
n.UHDK_KEY_HOME => {
|
||||||
|
choose(p, null);
|
||||||
|
move(p, true);
|
||||||
|
},
|
||||||
|
n.UHDK_KEY_END => {
|
||||||
|
choose(p, null);
|
||||||
|
move(p, false);
|
||||||
|
},
|
||||||
|
n.UHDK_KEY_RIGHT => {
|
||||||
|
openChild(p);
|
||||||
|
if (s().top != p and s().top != null) move(s().top.?, true);
|
||||||
|
},
|
||||||
|
n.UHDK_KEY_LEFT => if (p.parent) |parent| {
|
||||||
|
closeChildren(parent);
|
||||||
|
s().hover_at = 0;
|
||||||
|
},
|
||||||
|
n.UHDK_KEY_ENTER, ' ' => select(p),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scroll(delta: f32) void {
|
||||||
|
const p = s().pointer orelse return;
|
||||||
|
closeChildren(p);
|
||||||
|
p.scroll = std.math.clamp(p.scroll + @as(i32, @intFromFloat(std.math.clamp(delta, -480, 480))), 0, @max(0, p.total - p.height));
|
||||||
|
p.selected = null;
|
||||||
|
p.pressed = null;
|
||||||
|
p.dirty = true;
|
||||||
|
s().hover_at = 0;
|
||||||
|
}
|
||||||
|
pub fn flags() void {
|
||||||
|
var cursor = s().top;
|
||||||
|
while (cursor) |p| : (cursor = p.parent) {
|
||||||
|
p.dirty = true;
|
||||||
|
if (p.selected) |index| if (!usable(p.menu.payload.menu.items[index])) {
|
||||||
|
closeChildren(p);
|
||||||
|
p.selected = null;
|
||||||
|
p.pressed = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn tick() i32 {
|
||||||
|
if (!active()) return -1;
|
||||||
|
const now = runtime.now(b().app.io);
|
||||||
|
if (s().hover_at != 0 and now >= s().hover_at) {
|
||||||
|
if (s().pointer) |p| openChild(p);
|
||||||
|
s().hover_at = 0;
|
||||||
|
}
|
||||||
|
var cursor = s().top;
|
||||||
|
var timeout: i32 = if (s().hover_at != 0) 25 else -1;
|
||||||
|
while (cursor) |p| : (cursor = p.parent) {
|
||||||
|
if (!p.configured) {
|
||||||
|
if (now >= p.deadline) {
|
||||||
|
finish(abi.UNAVAILABLE);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
timeout = 100;
|
||||||
|
}
|
||||||
|
render(p) catch {
|
||||||
|
finish(abi.OOM);
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
@ -6,10 +6,10 @@ 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,
|
Keyboard/pointer input, cursor shapes, XKB composition, text-input-v3 IME,
|
||||||
clipboard, incoming file drops, portal dialogs/URLs/preferences, activation,
|
clipboard, incoming file drops, portal dialogs/URLs/preferences, activation,
|
||||||
application icons, inotify file watches, and Plasma app/window menu export are
|
application icons, inotify file watches, Plasma app/window menu export, and
|
||||||
also implemented.
|
software-rendered context menus are also implemented.
|
||||||
|
|
||||||
Native context menus, a cursor-theme fallback, client-side window decorations, and X11
|
A cursor-theme fallback, client-side window decorations, and X11
|
||||||
remain unsupported. Protocol tests cover IME batching; a live Fcitx/IBus CJK
|
remain unsupported. Protocol tests cover IME batching; a live Fcitx/IBus CJK
|
||||||
session and other desktops still need qualification. Missing optional protocols
|
session and other desktops still need qualification. Missing optional protocols
|
||||||
or libraries disable their features without a desktop-name whitelist.
|
or libraries disable their features without a desktop-name whitelist.
|
||||||
|
|
@ -174,6 +174,29 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
||||||
window. Session-bus loss removes exports/capability but keeps local shortcuts;
|
window. Session-bus loss removes exports/capability but keeps local shortcuts;
|
||||||
reconnecting to a replaced session bus requires restarting the application.
|
reconnecting to a replaced session bus requires restarting the application.
|
||||||
Restarting just the menu consumer does not require reattachment.
|
Restarting just the menu consumer does not require reattachment.
|
||||||
|
- Context menus use an internal software renderer and `xdg_popup`, with a
|
||||||
|
checked-in 16px Roboto bitmap (about 24 KB). They need `wl_shm` and a seat,
|
||||||
|
independently of KDE appmenu export or a session bus. No Qt/GTK, runtime font
|
||||||
|
library, image decoder, or application GL context is used for menu drawing.
|
||||||
|
`CONTEXT_MENU` reports protocol availability; opening also requires a visible,
|
||||||
|
configured parent and a recent button/key press from that window. Call the
|
||||||
|
popup API while handling that input event; missing provenance returns
|
||||||
|
`UNAVAILABLE`. One popup request can be active; another returns `BUSY`.
|
||||||
|
- Context menus display labels, shortcut hints, checked/disabled items and
|
||||||
|
separators. Pointer hover or Right opens submenus, Left closes a submenu,
|
||||||
|
Up/Down/Home/End navigate, Enter/Space select, and Escape dismisses. Tall menus
|
||||||
|
scroll with the wheel or keyboard. Positioners let the compositor adjust menus
|
||||||
|
at screen edges. Popup input is consumed; activation emits its menu/edit/quit
|
||||||
|
event before successful request completion. Dismissal, cancellation, parent
|
||||||
|
hiding/destruction and seat removal complete as `CANCELLED`.
|
||||||
|
- This small menu has its own light/dark palette, selected from portal appearance
|
||||||
|
preferences when available. It does not reproduce arbitrary KDE widget themes
|
||||||
|
or expose an accessibility tree. The bitmap covers printable ASCII, Latin-1
|
||||||
|
and selected punctuation/arrows, with a replacement glyph for other text; no
|
||||||
|
shaping or bidi is implemented. Output scales enlarge the 16px bitmap instead
|
||||||
|
of rerasterizing it. Font provenance, license and maintainer regeneration
|
||||||
|
instructions are in [font/README.md](font/README.md). The renderer and font stay
|
||||||
|
entirely inside the Linux backend.
|
||||||
- File watches use inotify. A document watch follows its named path through
|
- File watches use inotify. A document watch follows its named path through
|
||||||
atomic replacement by observing its parent. Directory watches observe immediate
|
atomic replacement by observing its parent. Directory watches observe immediate
|
||||||
children. Queue overflow requests a rescan; lost roots are invalidated. Each
|
children. Queue overflow requests a rescan; lost roots are invalidated. Each
|
||||||
|
|
@ -194,10 +217,12 @@ python3 src/linux/tests/disconnect.py zig-out/bin/linux-native-tests
|
||||||
python3 src/linux/tests/wayland_fixture.py zig-out/bin/linux-service-tests
|
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
|
python3 src/linux/tests/portal_fixture.py zig-out/bin/linux-service-tests
|
||||||
python3 src/linux/tests/menu_fixture.py zig-out/bin/linux-service-tests
|
python3 src/linux/tests/menu_fixture.py zig-out/bin/linux-service-tests
|
||||||
|
python3 src/linux/tests/popup_fixture.py zig-out/bin/linux-service-tests
|
||||||
# Opens a supplied app that exports a Quit or Quit … item, then activates that item:
|
# Opens a supplied app that exports a Quit or Quit … item, then activates that item:
|
||||||
python3 src/linux/tests/menu_live.py /path/to/menu-enabled-app
|
python3 src/linux/tests/menu_live.py /path/to/menu-enabled-app
|
||||||
zig-out/bin/linux-service-tests desktop # live portal discovery/icons/file watches
|
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
|
||||||
|
python3 src/linux/tools/bitmap-font.py --check
|
||||||
```
|
```
|
||||||
|
|
||||||
The native harness exercises repeated application/window lifetimes, cancellation,
|
The native harness exercises repeated application/window lifetimes, cancellation,
|
||||||
|
|
@ -212,6 +237,12 @@ keys/repeat/dead-key composition, IME revisions/deletion/preedit, scrolling,
|
||||||
clipboard size limits/cancellation/invalid UTF-8/early reader exit, URI drops,
|
clipboard size limits/cancellation/invalid UTF-8/early reader exit, URI drops,
|
||||||
activation, portal open/save options, live settings, response-before-reply,
|
activation, portal open/save options, live settings, response-before-reply,
|
||||||
cancellation with a late alternate handle, parent destruction and portal restart.
|
cancellation with a late alternate handle, parent destruction and portal restart.
|
||||||
|
The popup fixture checks shared-memory pixels, configure/scale handling,
|
||||||
|
press-serial grabs, nested teardown order, disabled items, pointer/keyboard
|
||||||
|
selection, scrolling, live flags, cancellation, outside clicks, parent destruction
|
||||||
|
and input suppression across focus restoration. It saves a rendered menu to
|
||||||
|
`/tmp/uhdk-popup.ppm` for inspection. Live right-click behavior still needs a
|
||||||
|
manual compositor check; the fixture does not establish that by itself.
|
||||||
The menu fixture covers D-BusMenu layout/properties/errors, state signals, window
|
The menu fixture covers D-BusMenu layout/properties/errors, state signals, window
|
||||||
overrides/defaults, retained trees, stale paths, consumer restarts, 64-bit command
|
overrides/defaults, retained trees, stale paths, consumer restarts, 64-bit command
|
||||||
IDs, role dispatch, shortcut consumption, missing protocol/bus and bus loss. The live menu check verifies KWin
|
IDs, role dispatch, shortcut consumption, missing protocol/bus and bus loss. The live menu check verifies KWin
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const Desktop = @import("Desktop.zig");
|
||||||
const Portal = @import("Portal.zig");
|
const Portal = @import("Portal.zig");
|
||||||
const Watch = @import("Watch.zig");
|
const Watch = @import("Watch.zig");
|
||||||
const Data = @import("Data.zig");
|
const Data = @import("Data.zig");
|
||||||
|
const Popup = @import("Popup.zig");
|
||||||
const KdeAppMenu = @import("KdeAppMenu.zig");
|
const KdeAppMenu = @import("KdeAppMenu.zig");
|
||||||
const Text = @import("Text.zig");
|
const Text = @import("Text.zig");
|
||||||
|
|
||||||
|
|
@ -70,6 +71,7 @@ pub const Backend = struct {
|
||||||
portal: Portal.State = .{},
|
portal: Portal.State = .{},
|
||||||
desktop: Desktop.State = .{},
|
desktop: Desktop.State = .{},
|
||||||
menus: KdeAppMenu.State = .{},
|
menus: KdeAppMenu.State = .{},
|
||||||
|
popup: Popup.State = .{},
|
||||||
capabilities: std.atomic.Value(u32) = .init(0),
|
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),
|
||||||
|
|
@ -182,6 +184,7 @@ 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);
|
while (b.watches) |w| Watch.destroy(w);
|
||||||
|
Popup.deinit();
|
||||||
Desktop.deinit();
|
Desktop.deinit();
|
||||||
KdeAppMenu.deinit();
|
KdeAppMenu.deinit();
|
||||||
Portal.deinit();
|
Portal.deinit();
|
||||||
|
|
@ -226,6 +229,7 @@ fn fail(b: *Backend) void {
|
||||||
b.app.lock();
|
b.app.lock();
|
||||||
b.app.stopping = true;
|
b.app.stopping = true;
|
||||||
b.app.unlock();
|
b.app.unlock();
|
||||||
|
Popup.deinit();
|
||||||
runtime.simpleEvent(b.app, n.UHDK_EVENT_QUIT_REQUESTED);
|
runtime.simpleEvent(b.app, n.UHDK_EVENT_QUIT_REQUESTED);
|
||||||
}
|
}
|
||||||
export fn z_native_run() void {
|
export fn z_native_run() void {
|
||||||
|
|
@ -238,6 +242,8 @@ export fn z_native_run() void {
|
||||||
var cursor = b.windows;
|
var cursor = b.windows;
|
||||||
var timeout: c_int = Input.tick();
|
var timeout: c_int = Input.tick();
|
||||||
Watch.tick();
|
Watch.tick();
|
||||||
|
const popup_timeout = Popup.tick();
|
||||||
|
if (popup_timeout >= 0) timeout = if (timeout < 0) popup_timeout else @min(timeout, popup_timeout);
|
||||||
const desktop_timeout = Desktop.tick();
|
const desktop_timeout = Desktop.tick();
|
||||||
if (desktop_timeout >= 0) timeout = if (timeout < 0) desktop_timeout else @min(timeout, desktop_timeout);
|
if (desktop_timeout >= 0) timeout = if (timeout < 0) desktop_timeout else @min(timeout, desktop_timeout);
|
||||||
const portal_timeout = Portal.tick();
|
const portal_timeout = Portal.tick();
|
||||||
|
|
@ -316,6 +322,7 @@ fn registryGlobal(data: ?*anyopaque, registry: ?*c.struct_wl_registry, name: u32
|
||||||
Portal.global(registry, name, str, version);
|
Portal.global(registry, name, str, version);
|
||||||
KdeAppMenu.global(registry, name, str, version);
|
KdeAppMenu.global(registry, name, str, version);
|
||||||
Desktop.global(registry, name, str, version);
|
Desktop.global(registry, name, str, version);
|
||||||
|
Popup.capability();
|
||||||
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")) {
|
||||||
|
|
@ -523,6 +530,7 @@ 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);
|
||||||
|
Popup.forget(w);
|
||||||
KdeAppMenu.forget(w);
|
KdeAppMenu.forget(w);
|
||||||
Desktop.forget(w);
|
Desktop.forget(w);
|
||||||
Data.forget(w);
|
Data.forget(w);
|
||||||
|
|
@ -557,7 +565,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_APP_MENU) return KdeAppMenu.attach(null, cmd.other);
|
if (cmd.op == n.Z_APP_MENU) return KdeAppMenu.attach(null, cmd.other);
|
||||||
if (cmd.op == n.Z_MENU_FLAGS) return KdeAppMenu.updateFlags();
|
if (cmd.op == n.Z_MENU_FLAGS) {
|
||||||
|
Popup.flags();
|
||||||
|
return KdeAppMenu.updateFlags();
|
||||||
|
}
|
||||||
|
if (cmd.op == n.Z_POPUP) return Popup.request(cmd, id);
|
||||||
if (cmd.op == n.Z_ICON) return Desktop.setIcon(cmd.image);
|
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_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_WATCH) return Watch.create(cmd, &result.*);
|
||||||
|
|
@ -603,6 +615,10 @@ export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64,
|
||||||
},
|
},
|
||||||
n.Z_VISIBLE => {
|
n.Z_VISIBLE => {
|
||||||
w.want_visible = cmd.number != 0;
|
w.want_visible = cmd.number != 0;
|
||||||
|
if (!w.want_visible) {
|
||||||
|
Popup.forget(w);
|
||||||
|
Input.clearPopupTrigger(w);
|
||||||
|
}
|
||||||
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;
|
||||||
},
|
},
|
||||||
|
|
@ -621,7 +637,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;
|
if (Popup.cancel(id) or 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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Strict private Wayland popup peer; captures actual pixels to /tmp/uhdk-popup.ppm.
|
||||||
|
|
||||||
|
No session bus, EGL rendering, or desktop input injection is used.
|
||||||
|
"""
|
||||||
|
import mmap
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from wayland_fixture import Compositor, run
|
||||||
|
|
||||||
|
|
||||||
|
class Popups(Compositor):
|
||||||
|
globals = Compositor.globals + [('wl_shm', 1)]
|
||||||
|
|
||||||
|
def __init__(self, sock, inject=False):
|
||||||
|
super().__init__(sock, False)
|
||||||
|
self.stage = -1
|
||||||
|
self.tasks = []
|
||||||
|
self.positions = {}
|
||||||
|
self.popup_surfaces = {}
|
||||||
|
self.xdg_surfaces = {}
|
||||||
|
self.stack = []
|
||||||
|
self.pools = {}
|
||||||
|
self.buffers = {}
|
||||||
|
self.attached = {}
|
||||||
|
self.trigger_serial = 500
|
||||||
|
|
||||||
|
def schedule(self, delay, action):
|
||||||
|
self.tasks.append((time.monotonic() + delay, action))
|
||||||
|
|
||||||
|
def timers(self):
|
||||||
|
ready = [x for x in self.tasks if x[0] <= time.monotonic()]
|
||||||
|
self.tasks = [x for x in self.tasks if x[0] > time.monotonic()]
|
||||||
|
for _, action in ready: action()
|
||||||
|
|
||||||
|
def keyboard(self, code):
|
||||||
|
keyboard = self.ids['wl_keyboard']
|
||||||
|
self.event(keyboard, 'key', 551, 1, code, 1)
|
||||||
|
self.event(keyboard, 'key', 552, 2, code, 0)
|
||||||
|
|
||||||
|
def pointer(self, surface, x, y, click=False):
|
||||||
|
pointer = self.ids['wl_pointer']
|
||||||
|
self.event(pointer, 'enter', 550, surface, x * 256, y * 256)
|
||||||
|
if click:
|
||||||
|
self.event(pointer, 'button', 551, 1, 0x110, 1)
|
||||||
|
self.event(pointer, 'button', 552, 2, 0x110, 0)
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
surface = next(iter(self.surfaces))
|
||||||
|
self.event(self.ids['wl_keyboard'], 'enter', 499, surface, b'')
|
||||||
|
self.event(self.ids['wl_keyboard'], 'modifiers', 499, 0, 0, 0, 0)
|
||||||
|
self.event(self.ids['wl_pointer'], 'enter', 499, surface, 12 * 256, 16 * 256)
|
||||||
|
self.trigger_serial += 10
|
||||||
|
self.event(self.ids['wl_pointer'], 'button', self.trigger_serial, 1, 0x111, 1)
|
||||||
|
self.event(self.ids['wl_pointer'], 'button', self.trigger_serial + 1, 2, 0x111, 0)
|
||||||
|
|
||||||
|
def actions(self, surface, p):
|
||||||
|
self.event(self.ids['wl_keyboard'], 'enter', 549, surface, b'')
|
||||||
|
self.event(self.ids['wl_keyboard'], 'modifiers', 549, 0, 0, 0, 0)
|
||||||
|
if p['child']:
|
||||||
|
if self.stage == 10: self.schedule(.05, lambda: self.pointer(surface, 35, 15, True))
|
||||||
|
else: self.schedule(.05, lambda: self.keyboard(28))
|
||||||
|
elif self.stage == 0:
|
||||||
|
# Split popup geometry from the surface configure. Hover dirties the
|
||||||
|
# menu during the gap; the client must wait for the configure serial.
|
||||||
|
width, height = p['size']
|
||||||
|
self.event(p['popup'], 'configure', 10, 20, width + 4, height)
|
||||||
|
p['pending_configure'] = True
|
||||||
|
def acknowledge():
|
||||||
|
p['pending_configure'] = False
|
||||||
|
self.serial += 1
|
||||||
|
self.event(p['xdg'], 'configure', self.serial)
|
||||||
|
self.schedule(.06, acknowledge)
|
||||||
|
self.pointer(surface, 35, 15)
|
||||||
|
self.schedule(.12, lambda: self.pointer(surface, 35, 15, True))
|
||||||
|
elif self.stage == 1:
|
||||||
|
self.keyboard(108)
|
||||||
|
self.keyboard(108) # Skip disabled/separator; select Checked.
|
||||||
|
self.schedule(.05, lambda: self.event(self.ids['wl_keyboard'], 'key', 551, 1, 28, 1))
|
||||||
|
elif self.stage == 2:
|
||||||
|
self.keyboard(107) # End = Copy.
|
||||||
|
self.keyboard(103) # Up = More.
|
||||||
|
self.schedule(.05, lambda: self.keyboard(106))
|
||||||
|
elif self.stage == 3:
|
||||||
|
self.keyboard(1)
|
||||||
|
elif self.stage == 4:
|
||||||
|
self.event(p['popup'], 'popup_done')
|
||||||
|
elif self.stage == 6:
|
||||||
|
self.schedule(.2, lambda: self.pointer(surface, 35, 15, True))
|
||||||
|
self.schedule(.3, lambda: self.keyboard(1))
|
||||||
|
elif self.stage in (7, 8):
|
||||||
|
self.keyboard(107) # Copy, or final item in the tall scrolling menu.
|
||||||
|
self.schedule(.05, lambda: self.keyboard(28))
|
||||||
|
elif self.stage == 9:
|
||||||
|
self.pointer(next(iter(self.surfaces)), 15, 15, True)
|
||||||
|
elif self.stage == 10:
|
||||||
|
self.pointer(surface, 35, 102) # Hover opens More after a short delay.
|
||||||
|
# Stages 5/11 are cancelled by the application after 100ms.
|
||||||
|
|
||||||
|
def request(self, obj, iface, name, args):
|
||||||
|
if iface == 'wl_surface' and name == 'commit' and obj in self.popup_surfaces:
|
||||||
|
p = self.popup_surfaces[obj]
|
||||||
|
assert not p.get('pending_configure', False), 'Commit before xdg_surface.configure'
|
||||||
|
if not p['configured']:
|
||||||
|
p['configured'] = True
|
||||||
|
width, height = p['size']
|
||||||
|
if self.stage == 8: height = 120
|
||||||
|
if self.stage == 1: self.event(obj, 'preferred_buffer_scale', 2)
|
||||||
|
self.event(p['popup'], 'configure', 10, 20, width, height)
|
||||||
|
self.serial += 1
|
||||||
|
self.event(p['xdg'], 'configure', self.serial)
|
||||||
|
elif obj in self.attached:
|
||||||
|
buf = self.attached[obj]
|
||||||
|
meta = self.buffers[buf]
|
||||||
|
pixels = bytes(meta['mapping'][meta['offset']:meta['offset'] + meta['stride'] * meta['height']])
|
||||||
|
words = struct.unpack('=' + 'I' * (len(pixels) // 4), pixels)
|
||||||
|
colors = set(words)
|
||||||
|
assert len(colors) > 15, 'Expected antialiased bitmap text in wl_shm pixels'
|
||||||
|
assert all((color >> 24) == 255 for color in colors)
|
||||||
|
if self.stage == 0 and not p['child']:
|
||||||
|
rgb = bytes(channel for color in words for channel in [(color >> 16) & 255, (color >> 8) & 255, color & 255])
|
||||||
|
Path('/tmp/uhdk-popup.ppm').write_bytes(f"P6\n{meta['width']} {meta['height']}\n255\n".encode() + rgb)
|
||||||
|
if self.stage == 1: assert meta['width'] == p['size'][0] * 2
|
||||||
|
def release():
|
||||||
|
if buf in self.objects: self.event(buf, 'release')
|
||||||
|
self.schedule(.03, release)
|
||||||
|
if not p['mapped']:
|
||||||
|
p['mapped'] = True
|
||||||
|
self.schedule(.03, lambda: self.actions(obj, p))
|
||||||
|
return
|
||||||
|
super().request(obj, iface, name, args)
|
||||||
|
if iface == 'xdg_toplevel' and name == 'set_title' and args[0].startswith('popup-stage-'):
|
||||||
|
assert not self.stack
|
||||||
|
self.stage = int(args[0].split('-')[-1])
|
||||||
|
self.schedule(.03, self.trigger)
|
||||||
|
elif iface == 'xdg_wm_base' and name == 'get_xdg_surface':
|
||||||
|
self.xdg_surfaces[args[0]] = args[1]
|
||||||
|
elif iface == 'xdg_wm_base' and name == 'create_positioner':
|
||||||
|
self.positions[args[0]] = {}
|
||||||
|
elif iface == 'xdg_positioner':
|
||||||
|
if name == 'set_size': self.positions[obj]['size'] = tuple(args)
|
||||||
|
elif name == 'set_anchor_rect': self.positions[obj]['anchor'] = tuple(args)
|
||||||
|
elif iface == 'xdg_surface' and name == 'get_popup':
|
||||||
|
popup, parent, position = args
|
||||||
|
surface = self.xdg_surfaces[obj]
|
||||||
|
self.popup_surfaces[surface] = dict(popup=popup, xdg=obj, configured=False, mapped=False, child=bool(self.stack), **self.positions[position])
|
||||||
|
if self.stack: assert parent == self.stack[-1][1]
|
||||||
|
self.stack.append((popup, obj, surface))
|
||||||
|
elif iface == 'xdg_popup' and name == 'grab':
|
||||||
|
assert args[0] == self.ids['wl_seat'] and args[1] != 0
|
||||||
|
assert self.stack[-1][0] == obj
|
||||||
|
if len(self.stack) == 1: assert args[1] == self.trigger_serial, 'Must use press serial'
|
||||||
|
elif iface == 'xdg_popup' and name == 'destroy':
|
||||||
|
assert self.stack[-1][0] == obj, 'Destroy nested popups from the top down'
|
||||||
|
_, _, surface = self.stack.pop()
|
||||||
|
del self.popup_surfaces[surface]
|
||||||
|
if self.stage == 1:
|
||||||
|
keyboard = self.ids['wl_keyboard']
|
||||||
|
parent = next(iter(self.surfaces))
|
||||||
|
self.event(keyboard, 'leave', 552, surface)
|
||||||
|
self.event(keyboard, 'enter', 553, parent, struct.pack('=I', 28))
|
||||||
|
self.event(keyboard, 'modifiers', 553, 0, 0, 0, 0)
|
||||||
|
self.event(keyboard, 'key', 554, 2, 28, 0) # Consumed key's release stays consumed.
|
||||||
|
elif iface == 'wl_shm' and name == 'create_pool':
|
||||||
|
pool, fd, size = args
|
||||||
|
self.pools[pool] = mmap.mmap(fd, size, access=mmap.ACCESS_READ)
|
||||||
|
os.close(fd)
|
||||||
|
elif iface == 'wl_shm_pool' and name == 'create_buffer':
|
||||||
|
buf, offset, width, height, stride, fmt = args
|
||||||
|
assert fmt == 0 and stride == width * 4
|
||||||
|
self.buffers[buf] = dict(mapping=self.pools[obj], offset=offset, width=width, height=height, stride=stride)
|
||||||
|
elif iface == 'wl_surface' and name == 'attach':
|
||||||
|
self.attached[obj] = args[0]
|
||||||
|
elif iface == 'wl_buffer' and name == 'destroy':
|
||||||
|
self.buffers.pop(obj, None)
|
||||||
|
elif iface == 'wl_surface' and name == 'destroy':
|
||||||
|
self.surfaces.pop(obj, None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
environment = dict(os.environ)
|
||||||
|
environment.pop('DBUS_SESSION_BUS_ADDRESS', None)
|
||||||
|
run(sys.argv[1], 'popup', environment, Popups)
|
||||||
|
print('Wayland popup grab/stack/configure/scale and software pixels: PASS')
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* Included by services.c; driven by popup_fixture.py on a private compositor. */
|
||||||
|
#define POPUP_OPEN UINT64_C(0x1234567800000001)
|
||||||
|
#define POPUP_CHILD UINT64_C(0xfedcba9800000002)
|
||||||
|
static void popups(uhdk_app *app) {
|
||||||
|
uhdk_window *w = window(app);
|
||||||
|
wait_capability(app, UHDK_CAP_CONTEXT_MENU);
|
||||||
|
assert(!uhdk_app_has_capability(app, UHDK_CAP_APP_MENU));
|
||||||
|
uint64_t id;
|
||||||
|
OK(uhdk_window_set_visible_async(w, 1, &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
OK(uhdk_window_set_text_input_async(w, 1, &id));
|
||||||
|
OK(wait_result(app, id).status);
|
||||||
|
uhdk_menu_item child_items[] = {{.command_id = POPUP_CHILD, .label = {"Child café", 11}}};
|
||||||
|
uhdk_menu *child, *root;
|
||||||
|
OK(uhdk_menu_create(app, child_items, 1, &child));
|
||||||
|
uhdk_menu_item items[] = {
|
||||||
|
{.command_id = POPUP_OPEN,
|
||||||
|
.label = {"Open…", 7},
|
||||||
|
.shortcut_key = 'o',
|
||||||
|
.shortcut_modifiers = UHDK_MOD_CONTROL},
|
||||||
|
{.command_id = 102, .label = {"Disabled", 8}, .flags = UHDK_MENU_DISABLED},
|
||||||
|
{.flags = UHDK_MENU_SEPARATOR},
|
||||||
|
{.command_id = 103, .label = {"Checked", 7}, .flags = UHDK_MENU_CHECKED},
|
||||||
|
{.command_id = 104, .label = {"More", 4}, .submenu = child},
|
||||||
|
{.label = {"Copy", 4}, .role = UHDK_MENU_ROLE_COPY},
|
||||||
|
};
|
||||||
|
OK(uhdk_menu_create(app, items, 6, &root));
|
||||||
|
uhdk_menu_release(child);
|
||||||
|
OK(uhdk_menu_popup_async(root, w, (uhdk_point){4, 4}, &id));
|
||||||
|
assert(wait_result(app, id).status == UHDK_STATUS_UNAVAILABLE); // No input action yet.
|
||||||
|
for (unsigned stage = 0; stage < 12; ++stage) {
|
||||||
|
uhdk_menu *menu = root;
|
||||||
|
if (stage == 8) {
|
||||||
|
uhdk_menu_item many[32] = {0};
|
||||||
|
for (unsigned j = 0; j < 32; ++j) {
|
||||||
|
many[j].command_id = 1000 + j;
|
||||||
|
many[j].label = text("Scrollable item");
|
||||||
|
}
|
||||||
|
OK(uhdk_menu_create(app, many, 32, &menu));
|
||||||
|
}
|
||||||
|
char title[64];
|
||||||
|
snprintf(title, sizeof(title), "popup-stage-%u", stage);
|
||||||
|
OK(uhdk_window_set_title_async(w, text(title), &id));
|
||||||
|
// Do not discard the input event that may arrive with title completion.
|
||||||
|
uint64_t popup = 0, busy = 0, started = 0;
|
||||||
|
unsigned commands = 0, complete = 0, busy_done = 0, changed = 0;
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (!complete && uhdk_app_get_time_ns(app) < end) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 20000000, &events));
|
||||||
|
for (uint64_t j = 0; j < events.len; ++j) {
|
||||||
|
const uhdk_event *e = &events.ptr[j];
|
||||||
|
if (e->type == UHDK_EVENT_POINTER_BUTTON) {
|
||||||
|
const uhdk_pointer_event *p = e->data;
|
||||||
|
assert(p->button == UHDK_BUTTON_SECONDARY);
|
||||||
|
if (p->pressed) {
|
||||||
|
assert(popup == 0);
|
||||||
|
OK(uhdk_menu_popup_async(menu, w, p->position, &popup));
|
||||||
|
started = uhdk_app_get_time_ns(app);
|
||||||
|
if (stage == 0)
|
||||||
|
OK(uhdk_menu_popup_async(menu, w, p->position, &busy));
|
||||||
|
}
|
||||||
|
} else if (e->type == UHDK_EVENT_MENU_COMMAND) {
|
||||||
|
uint64_t command = ((const uhdk_menu_event *)e->data)->command_id;
|
||||||
|
uint64_t expected = stage == 0 ? POPUP_OPEN
|
||||||
|
: stage == 1 ? 103
|
||||||
|
: (stage == 2 || stage == 10) ? POPUP_CHILD
|
||||||
|
: 1031;
|
||||||
|
assert(stage == 0 || stage == 1 || stage == 2 || stage == 8 || stage == 10);
|
||||||
|
assert(command == expected && e->window == w);
|
||||||
|
++commands;
|
||||||
|
} else if (e->type == UHDK_EVENT_EDIT_COMMAND) {
|
||||||
|
assert(stage == 7 && e->window == w && e->input_id != 0);
|
||||||
|
assert(((const uhdk_edit_event *)e->data)->command == UHDK_EDIT_COPY);
|
||||||
|
++commands;
|
||||||
|
} else if (e->type == UHDK_EVENT_REQUEST_DONE) {
|
||||||
|
const uhdk_request_result *r = e->data;
|
||||||
|
if (r->id == popup && popup) {
|
||||||
|
int selected = stage <= 2 || stage == 7 || stage == 8 || stage == 10;
|
||||||
|
assert(r->status == (selected ? UHDK_STATUS_OK : UHDK_STATUS_CANCELLED));
|
||||||
|
assert(commands == (unsigned)selected); // Ordering and exactly one action.
|
||||||
|
++complete;
|
||||||
|
} else if (r->id == busy && busy) {
|
||||||
|
assert(r->status == UHDK_STATUS_BUSY);
|
||||||
|
++busy_done;
|
||||||
|
} else
|
||||||
|
OK(r->status);
|
||||||
|
} else if (e->type == UHDK_EVENT_KEY || e->type == UHDK_EVENT_TEXT_COMMIT)
|
||||||
|
assert(!"popup input leaked to application");
|
||||||
|
}
|
||||||
|
if (popup && !changed && uhdk_app_get_time_ns(app) - started > 100000000ull) {
|
||||||
|
if (stage == 5)
|
||||||
|
OK(uhdk_request_cancel(app, popup));
|
||||||
|
if (stage == 6) {
|
||||||
|
OK(uhdk_menu_set_item_flags_async(root, POPUP_OPEN, UHDK_MENU_DISABLED, &id));
|
||||||
|
OK(uhdk_menu_set_item_flags_async(root, 103, 0, &id));
|
||||||
|
}
|
||||||
|
if (stage == 11)
|
||||||
|
uhdk_window_destroy(w);
|
||||||
|
changed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(complete == 1);
|
||||||
|
if (stage == 0)
|
||||||
|
assert(busy_done == 1);
|
||||||
|
if (stage == 8)
|
||||||
|
uhdk_menu_release(menu);
|
||||||
|
}
|
||||||
|
uhdk_menu_release(root);
|
||||||
|
puts("software popup selection/roles/submenus/cancellation/input/scroll/lifetime: PASS");
|
||||||
|
}
|
||||||
|
#undef POPUP_OPEN
|
||||||
|
#undef POPUP_CHILD
|
||||||
|
|
@ -364,8 +364,13 @@ static void input(uhdk_app *app) {
|
||||||
puts("input/IME/clipboard/drop/activation: PASS");
|
puts("input/IME/clipboard/drop/activation: PASS");
|
||||||
}
|
}
|
||||||
#include "menus.c"
|
#include "menus.c"
|
||||||
|
#include "popups.c"
|
||||||
|
|
||||||
static void run(uhdk_app *app, void *mode) {
|
static void run(uhdk_app *app, void *mode) {
|
||||||
|
if (strcmp(mode, "popup") == 0) {
|
||||||
|
popups(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (strcmp(mode, "menu-noprotocol") == 0 || strcmp(mode, "menu-nobus") == 0) {
|
if (strcmp(mode, "menu-noprotocol") == 0 || strcmp(mode, "menu-nobus") == 0) {
|
||||||
menus_unavailable(app, strcmp(mode, "menu-nobus") == 0);
|
menus_unavailable(app, strcmp(mode, "menu-nobus") == 0);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue