zig-wlroots/tinywl/tinywl.zig

596 lines
23 KiB
Zig

const std = @import("std");
const os = std.os;
const wl = @import("wayland").server.wl;
const wlr = @import("wlroots");
const xkb = @import("xkbcommon");
const gpa = std.heap.c_allocator;
pub fn main() anyerror!void {
wlr.log.init(.debug, null);
var server: Server = undefined;
try server.init();
defer server.deinit();
var buf: [11]u8 = undefined;
const socket = try server.wl_server.addSocketAuto(&buf);
if (os.argv.len >= 2) {
const cmd = std.mem.span(os.argv[1]);
var child = std.ChildProcess.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, gpa);
var env_map = try std.process.getEnvMap(gpa);
defer env_map.deinit();
try env_map.put("WAYLAND_DISPLAY", socket);
child.env_map = &env_map;
try child.spawn();
}
try server.backend.start();
std.log.info("Running compositor on WAYLAND_DISPLAY={s}", .{socket});
server.wl_server.run();
}
const Server = struct {
wl_server: *wl.Server,
backend: *wlr.Backend,
renderer: *wlr.Renderer,
allocator: *wlr.Allocator,
scene: *wlr.Scene,
output_layout: *wlr.OutputLayout,
scene_output_layout: *wlr.SceneOutputLayout,
new_output: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(newOutput),
xdg_shell: *wlr.XdgShell,
new_xdg_surface: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(newXdgSurface),
views: wl.list.Head(View, .link) = undefined,
seat: *wlr.Seat,
new_input: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(newInput),
request_set_cursor: wl.Listener(*wlr.Seat.event.RequestSetCursor) = wl.Listener(*wlr.Seat.event.RequestSetCursor).init(requestSetCursor),
request_set_selection: wl.Listener(*wlr.Seat.event.RequestSetSelection) = wl.Listener(*wlr.Seat.event.RequestSetSelection).init(requestSetSelection),
keyboards: wl.list.Head(Keyboard, .link) = undefined,
cursor: *wlr.Cursor,
cursor_mgr: *wlr.XcursorManager,
cursor_motion: wl.Listener(*wlr.Pointer.event.Motion) = wl.Listener(*wlr.Pointer.event.Motion).init(cursorMotion),
cursor_motion_absolute: wl.Listener(*wlr.Pointer.event.MotionAbsolute) = wl.Listener(*wlr.Pointer.event.MotionAbsolute).init(cursorMotionAbsolute),
cursor_button: wl.Listener(*wlr.Pointer.event.Button) = wl.Listener(*wlr.Pointer.event.Button).init(cursorButton),
cursor_axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(cursorAxis),
cursor_frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(cursorFrame),
cursor_mode: enum { passthrough, move, resize } = .passthrough,
grabbed_view: ?*View = null,
grab_x: f64 = 0,
grab_y: f64 = 0,
grab_box: wlr.Box = undefined,
resize_edges: wlr.Edges = .{},
fn init(server: *Server) !void {
const wl_server = try wl.Server.create();
const backend = try wlr.Backend.autocreate(wl_server, null);
const renderer = try wlr.Renderer.autocreate(backend);
const output_layout = try wlr.OutputLayout.create();
const scene = try wlr.Scene.create();
server.* = .{
.wl_server = wl_server,
.backend = backend,
.renderer = renderer,
.allocator = try wlr.Allocator.autocreate(backend, renderer),
.scene = scene,
.output_layout = output_layout,
.scene_output_layout = try scene.attachOutputLayout(output_layout),
.xdg_shell = try wlr.XdgShell.create(wl_server, 2),
.seat = try wlr.Seat.create(wl_server, "default"),
.cursor = try wlr.Cursor.create(),
.cursor_mgr = try wlr.XcursorManager.create(null, 24),
};
try server.renderer.initServer(wl_server);
_ = try wlr.Compositor.create(server.wl_server, 6, server.renderer);
_ = try wlr.Subcompositor.create(server.wl_server);
_ = try wlr.DataDeviceManager.create(server.wl_server);
server.backend.events.new_output.add(&server.new_output);
server.xdg_shell.events.new_surface.add(&server.new_xdg_surface);
server.views.init();
server.backend.events.new_input.add(&server.new_input);
server.seat.events.request_set_cursor.add(&server.request_set_cursor);
server.seat.events.request_set_selection.add(&server.request_set_selection);
server.keyboards.init();
server.cursor.attachOutputLayout(server.output_layout);
try server.cursor_mgr.load(1);
server.cursor.events.motion.add(&server.cursor_motion);
server.cursor.events.motion_absolute.add(&server.cursor_motion_absolute);
server.cursor.events.button.add(&server.cursor_button);
server.cursor.events.axis.add(&server.cursor_axis);
server.cursor.events.frame.add(&server.cursor_frame);
}
fn deinit(server: *Server) void {
server.wl_server.destroyClients();
server.wl_server.destroy();
}
fn newOutput(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
const server = @fieldParentPtr(Server, "new_output", listener);
if (!wlr_output.initRender(server.allocator, server.renderer)) return;
var state = wlr.Output.State.init();
defer state.finish();
state.setEnabled(true);
if (wlr_output.preferredMode()) |mode| {
state.setMode(mode);
}
if (!wlr_output.commitState(&state)) return;
Output.create(server, wlr_output) catch {
std.log.err("failed to allocate new output", .{});
wlr_output.destroy();
return;
};
}
fn newXdgSurface(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void {
const server = @fieldParentPtr(Server, "new_xdg_surface", listener);
switch (xdg_surface.role) {
.toplevel => {
// Don't add the view to server.views until it is mapped
const view = gpa.create(View) catch {
std.log.err("failed to allocate new view", .{});
return;
};
view.* = .{
.server = server,
.xdg_surface = xdg_surface,
.scene_tree = server.scene.tree.createSceneXdgSurface(xdg_surface) catch {
gpa.destroy(view);
std.log.err("failed to allocate new view", .{});
return;
},
};
view.scene_tree.node.data = @intFromPtr(view);
xdg_surface.data = @intFromPtr(view.scene_tree);
xdg_surface.surface.events.map.add(&view.map);
xdg_surface.surface.events.unmap.add(&view.unmap);
xdg_surface.events.destroy.add(&view.destroy);
xdg_surface.role_data.toplevel.?.events.request_move.add(&view.request_move);
xdg_surface.role_data.toplevel.?.events.request_resize.add(&view.request_resize);
},
.popup => {
// These asserts are fine since tinywl.zig doesn't support anything else that can
// make xdg popups (e.g. layer shell).
const parent = wlr.XdgSurface.tryFromWlrSurface(xdg_surface.role_data.popup.?.parent.?) orelse return;
const parent_tree = @as(?*wlr.SceneTree, @ptrFromInt(parent.data)) orelse {
// The xdg surface user data could be left null due to allocation failure.
return;
};
const scene_tree = parent_tree.createSceneXdgSurface(xdg_surface) catch {
std.log.err("failed to allocate xdg popup node", .{});
return;
};
xdg_surface.data = @intFromPtr(scene_tree);
},
.none => unreachable,
}
}
const ViewAtResult = struct {
view: *View,
surface: *wlr.Surface,
sx: f64,
sy: f64,
};
fn viewAt(server: *Server, lx: f64, ly: f64) ?ViewAtResult {
var sx: f64 = undefined;
var sy: f64 = undefined;
if (server.scene.tree.node.at(lx, ly, &sx, &sy)) |node| {
if (node.type != .buffer) return null;
const scene_buffer = wlr.SceneBuffer.fromNode(node);
const scene_surface = wlr.SceneSurface.tryFromBuffer(scene_buffer) orelse return null;
var it: ?*wlr.SceneTree = node.parent;
while (it) |n| : (it = n.node.parent) {
if (@as(?*View, @ptrFromInt(n.node.data))) |view| {
return ViewAtResult{
.view = view,
.surface = scene_surface.surface,
.sx = sx,
.sy = sy,
};
}
}
}
return null;
}
fn focusView(server: *Server, view: *View, surface: *wlr.Surface) void {
if (server.seat.keyboard_state.focused_surface) |previous_surface| {
if (previous_surface == surface) return;
if (wlr.XdgSurface.tryFromWlrSurface(previous_surface)) |xdg_surface| {
_ = xdg_surface.role_data.toplevel.?.setActivated(false);
}
}
view.scene_tree.node.raiseToTop();
view.link.remove();
server.views.prepend(view);
_ = view.xdg_surface.role_data.toplevel.?.setActivated(true);
const wlr_keyboard = server.seat.getKeyboard() orelse return;
server.seat.keyboardNotifyEnter(
surface,
wlr_keyboard.keycodes[0..wlr_keyboard.num_keycodes],
&wlr_keyboard.modifiers,
);
}
fn newInput(listener: *wl.Listener(*wlr.InputDevice), device: *wlr.InputDevice) void {
const server = @fieldParentPtr(Server, "new_input", listener);
switch (device.type) {
.keyboard => Keyboard.create(server, device) catch |err| {
std.log.err("failed to create keyboard: {}", .{err});
return;
},
.pointer => server.cursor.attachInputDevice(device),
else => {},
}
server.seat.setCapabilities(.{
.pointer = true,
.keyboard = server.keyboards.length() > 0,
});
}
fn requestSetCursor(
listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor),
event: *wlr.Seat.event.RequestSetCursor,
) void {
const server = @fieldParentPtr(Server, "request_set_cursor", listener);
if (event.seat_client == server.seat.pointer_state.focused_client)
server.cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y);
}
fn requestSetSelection(
listener: *wl.Listener(*wlr.Seat.event.RequestSetSelection),
event: *wlr.Seat.event.RequestSetSelection,
) void {
const server = @fieldParentPtr(Server, "request_set_selection", listener);
server.seat.setSelection(event.source, event.serial);
}
fn cursorMotion(
listener: *wl.Listener(*wlr.Pointer.event.Motion),
event: *wlr.Pointer.event.Motion,
) void {
const server = @fieldParentPtr(Server, "cursor_motion", listener);
server.cursor.move(event.device, event.delta_x, event.delta_y);
server.processCursorMotion(event.time_msec);
}
fn cursorMotionAbsolute(
listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute),
event: *wlr.Pointer.event.MotionAbsolute,
) void {
const server = @fieldParentPtr(Server, "cursor_motion_absolute", listener);
server.cursor.warpAbsolute(event.device, event.x, event.y);
server.processCursorMotion(event.time_msec);
}
fn processCursorMotion(server: *Server, time_msec: u32) void {
switch (server.cursor_mode) {
.passthrough => if (server.viewAt(server.cursor.x, server.cursor.y)) |res| {
server.seat.pointerNotifyEnter(res.surface, res.sx, res.sy);
server.seat.pointerNotifyMotion(time_msec, res.sx, res.sy);
} else {
server.cursor.setXcursor(server.cursor_mgr, "default");
server.seat.pointerClearFocus();
},
.move => {
const view = server.grabbed_view.?;
view.x = @as(i32, @intFromFloat(server.cursor.x - server.grab_x));
view.y = @as(i32, @intFromFloat(server.cursor.y - server.grab_y));
view.scene_tree.node.setPosition(view.x, view.y);
},
.resize => {
const view = server.grabbed_view.?;
const border_x = @as(i32, @intFromFloat(server.cursor.x - server.grab_x));
const border_y = @as(i32, @intFromFloat(server.cursor.y - server.grab_y));
var new_left = server.grab_box.x;
var new_right = server.grab_box.x + server.grab_box.width;
var new_top = server.grab_box.y;
var new_bottom = server.grab_box.y + server.grab_box.height;
if (server.resize_edges.top) {
new_top = border_y;
if (new_top >= new_bottom)
new_top = new_bottom - 1;
} else if (server.resize_edges.bottom) {
new_bottom = border_y;
if (new_bottom <= new_top)
new_bottom = new_top + 1;
}
if (server.resize_edges.left) {
new_left = border_x;
if (new_left >= new_right)
new_left = new_right - 1;
} else if (server.resize_edges.right) {
new_right = border_x;
if (new_right <= new_left)
new_right = new_left + 1;
}
var geo_box: wlr.Box = undefined;
view.xdg_surface.getGeometry(&geo_box);
view.x = new_left - geo_box.x;
view.y = new_top - geo_box.y;
view.scene_tree.node.setPosition(view.x, view.y);
const new_width = new_right - new_left;
const new_height = new_bottom - new_top;
_ = view.xdg_surface.role_data.toplevel.?.setSize(new_width, new_height);
},
}
}
fn cursorButton(
listener: *wl.Listener(*wlr.Pointer.event.Button),
event: *wlr.Pointer.event.Button,
) void {
const server = @fieldParentPtr(Server, "cursor_button", listener);
_ = server.seat.pointerNotifyButton(event.time_msec, event.button, event.state);
if (event.state == .released) {
server.cursor_mode = .passthrough;
} else if (server.viewAt(server.cursor.x, server.cursor.y)) |res| {
server.focusView(res.view, res.surface);
}
}
fn cursorAxis(
listener: *wl.Listener(*wlr.Pointer.event.Axis),
event: *wlr.Pointer.event.Axis,
) void {
const server = @fieldParentPtr(Server, "cursor_axis", listener);
server.seat.pointerNotifyAxis(
event.time_msec,
event.orientation,
event.delta,
event.delta_discrete,
event.source,
);
}
fn cursorFrame(listener: *wl.Listener(*wlr.Cursor), _: *wlr.Cursor) void {
const server = @fieldParentPtr(Server, "cursor_frame", listener);
server.seat.pointerNotifyFrame();
}
/// Assumes the modifier used for compositor keybinds is pressed
/// Returns true if the key was handled
fn handleKeybind(server: *Server, key: xkb.Keysym) bool {
switch (@intFromEnum(key)) {
// Exit the compositor
xkb.Keysym.Escape => server.wl_server.terminate(),
// Focus the next view in the stack, pushing the current top to the back
xkb.Keysym.F1 => {
if (server.views.length() < 2) return true;
const view = @fieldParentPtr(View, "link", server.views.link.prev.?);
server.focusView(view, view.xdg_surface.surface);
},
else => return false,
}
return true;
}
};
const Output = struct {
server: *Server,
wlr_output: *wlr.Output,
frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(frame),
request_state: wl.Listener(*wlr.Output.event.RequestState) =
wl.Listener(*wlr.Output.event.RequestState).init(request_state),
destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(destroy),
// The wlr.Output should be destroyed by the caller on failure to trigger cleanup.
fn create(server: *Server, wlr_output: *wlr.Output) !void {
const output = try gpa.create(Output);
output.* = .{
.server = server,
.wlr_output = wlr_output,
};
wlr_output.events.frame.add(&output.frame);
wlr_output.events.request_state.add(&output.request_state);
wlr_output.events.destroy.add(&output.destroy);
const layout_output = try server.output_layout.addAuto(wlr_output);
const scene_output = try server.scene.createSceneOutput(wlr_output);
server.scene_output_layout.addOutput(layout_output, scene_output);
}
fn frame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
const output = @fieldParentPtr(Output, "frame", listener);
const scene_output = output.server.scene.getSceneOutput(output.wlr_output).?;
_ = scene_output.commit(null);
var now: os.timespec = undefined;
os.clock_gettime(os.CLOCK.MONOTONIC, &now) catch @panic("CLOCK_MONOTONIC not supported");
scene_output.sendFrameDone(&now);
}
fn request_state(
listener: *wl.Listener(*wlr.Output.event.RequestState),
event: *wlr.Output.event.RequestState,
) void {
const output = @fieldParentPtr(Output, "request_state", listener);
_ = output.wlr_output.commitState(event.state);
}
fn destroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
const output = @fieldParentPtr(Output, "destroy", listener);
output.frame.link.remove();
output.destroy.link.remove();
gpa.destroy(output);
}
};
const View = struct {
server: *Server,
link: wl.list.Link = undefined,
xdg_surface: *wlr.XdgSurface,
scene_tree: *wlr.SceneTree,
x: i32 = 0,
y: i32 = 0,
map: wl.Listener(void) = wl.Listener(void).init(map),
unmap: wl.Listener(void) = wl.Listener(void).init(unmap),
destroy: wl.Listener(void) = wl.Listener(void).init(destroy),
request_move: wl.Listener(*wlr.XdgToplevel.event.Move) = wl.Listener(*wlr.XdgToplevel.event.Move).init(requestMove),
request_resize: wl.Listener(*wlr.XdgToplevel.event.Resize) = wl.Listener(*wlr.XdgToplevel.event.Resize).init(requestResize),
fn map(listener: *wl.Listener(void)) void {
const view = @fieldParentPtr(View, "map", listener);
view.server.views.prepend(view);
view.server.focusView(view, view.xdg_surface.surface);
}
fn unmap(listener: *wl.Listener(void)) void {
const view = @fieldParentPtr(View, "unmap", listener);
view.link.remove();
}
fn destroy(listener: *wl.Listener(void)) void {
const view = @fieldParentPtr(View, "destroy", listener);
view.map.link.remove();
view.unmap.link.remove();
view.destroy.link.remove();
view.request_move.link.remove();
view.request_resize.link.remove();
gpa.destroy(view);
}
fn requestMove(
listener: *wl.Listener(*wlr.XdgToplevel.event.Move),
_: *wlr.XdgToplevel.event.Move,
) void {
const view = @fieldParentPtr(View, "request_move", listener);
const server = view.server;
server.grabbed_view = view;
server.cursor_mode = .move;
server.grab_x = server.cursor.x - @as(f64, @floatFromInt(view.x));
server.grab_y = server.cursor.y - @as(f64, @floatFromInt(view.y));
}
fn requestResize(
listener: *wl.Listener(*wlr.XdgToplevel.event.Resize),
event: *wlr.XdgToplevel.event.Resize,
) void {
const view = @fieldParentPtr(View, "request_resize", listener);
const server = view.server;
server.grabbed_view = view;
server.cursor_mode = .resize;
server.resize_edges = event.edges;
var box: wlr.Box = undefined;
view.xdg_surface.getGeometry(&box);
const border_x = view.x + box.x + if (event.edges.right) box.width else 0;
const border_y = view.y + box.y + if (event.edges.bottom) box.height else 0;
server.grab_x = server.cursor.x - @as(f64, @floatFromInt(border_x));
server.grab_y = server.cursor.y - @as(f64, @floatFromInt(border_y));
server.grab_box = box;
server.grab_box.x += view.x;
server.grab_box.y += view.y;
}
};
const Keyboard = struct {
server: *Server,
link: wl.list.Link = undefined,
device: *wlr.InputDevice,
modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(modifiers),
key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key).init(key),
fn create(server: *Server, device: *wlr.InputDevice) !void {
const keyboard = try gpa.create(Keyboard);
errdefer gpa.destroy(keyboard);
keyboard.* = .{
.server = server,
.device = device,
};
const context = xkb.Context.new(.no_flags) orelse return error.ContextFailed;
defer context.unref();
const keymap = xkb.Keymap.newFromNames(context, null, .no_flags) orelse return error.KeymapFailed;
defer keymap.unref();
const wlr_keyboard = device.toKeyboard();
if (!wlr_keyboard.setKeymap(keymap)) return error.SetKeymapFailed;
wlr_keyboard.setRepeatInfo(25, 600);
wlr_keyboard.events.modifiers.add(&keyboard.modifiers);
wlr_keyboard.events.key.add(&keyboard.key);
server.seat.setKeyboard(wlr_keyboard);
server.keyboards.append(keyboard);
}
fn modifiers(listener: *wl.Listener(*wlr.Keyboard), wlr_keyboard: *wlr.Keyboard) void {
const keyboard = @fieldParentPtr(Keyboard, "modifiers", listener);
keyboard.server.seat.setKeyboard(wlr_keyboard);
keyboard.server.seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers);
}
fn key(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
const keyboard = @fieldParentPtr(Keyboard, "key", listener);
const wlr_keyboard = keyboard.device.toKeyboard();
// Translate libinput keycode -> xkbcommon
const keycode = event.keycode + 8;
var handled = false;
if (wlr_keyboard.getModifiers().alt and event.state == .pressed) {
for (wlr_keyboard.xkb_state.?.keyGetSyms(keycode)) |sym| {
if (keyboard.server.handleKeybind(sym)) {
handled = true;
break;
}
}
}
if (!handled) {
keyboard.server.seat.setKeyboard(wlr_keyboard);
keyboard.server.seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
}
}
};