From 939beef168ff0bb34fa7cec99ca075e3eb3975b1 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Mon, 1 Jun 2020 15:56:50 +0200 Subject: Split river and riverctl directories --- build.zig | 6 +- river/Box.zig | 34 ++ river/Config.zig | 284 ++++++++++++++ river/Control.zig | 139 +++++++ river/Cursor.zig | 412 ++++++++++++++++++++ river/Decoration.zig | 53 +++ river/DecorationManager.zig | 57 +++ river/InputManager.zig | 146 +++++++ river/Keyboard.zig | 180 +++++++++ river/LayerSurface.zig | 196 ++++++++++ river/Mapping.zig | 46 +++ river/Output.zig | 744 ++++++++++++++++++++++++++++++++++++ river/Root.zig | 247 ++++++++++++ river/Seat.zig | 312 +++++++++++++++ river/Server.zig | 279 ++++++++++++++ river/View.zig | 271 +++++++++++++ river/VoidView.zig | 48 +++ river/XdgPopup.zig | 82 ++++ river/XdgToplevel.zig | 201 ++++++++++ river/XwaylandUnmanaged.zig | 136 +++++++ river/XwaylandView.zig | 159 ++++++++ river/c.zig | 60 +++ river/command.zig | 122 ++++++ river/command/close.zig | 37 ++ river/command/declare_mode.zig | 51 +++ river/command/enter_mode.zig | 43 +++ river/command/exit.zig | 34 ++ river/command/focus.zig | 67 ++++ river/command/focus_all_tags.zig | 32 ++ river/command/focus_output.zig | 54 +++ river/command/focus_tag.zig | 37 ++ river/command/layout.zig | 37 ++ river/command/map.zig | 110 ++++++ river/command/mod_master_count.zig | 42 ++ river/command/mod_master_factor.zig | 45 +++ river/command/send_to_output.zig | 62 +++ river/command/spawn.zig | 47 +++ river/command/tag_view.zig | 43 +++ river/command/tag_view_all_tags.zig | 38 ++ river/command/toggle_float.zig | 38 ++ river/command/toggle_tag_focus.zig | 43 +++ river/command/toggle_view_tag.zig | 44 +++ river/command/zoom.zig | 54 +++ river/log.zig | 41 ++ river/main.zig | 42 ++ river/render.zig | 315 +++++++++++++++ river/test_main.zig | 20 + river/view_stack.zig | 406 ++++++++++++++++++++ riverctl/main.zig | 109 ++++++ src/Box.zig | 34 -- src/Config.zig | 284 -------------- src/Control.zig | 139 ------- src/Cursor.zig | 412 -------------------- src/Decoration.zig | 53 --- src/DecorationManager.zig | 57 --- src/InputManager.zig | 146 ------- src/Keyboard.zig | 180 --------- src/LayerSurface.zig | 196 ---------- src/Mapping.zig | 46 --- src/Output.zig | 744 ------------------------------------ src/Root.zig | 247 ------------ src/Seat.zig | 312 --------------- src/Server.zig | 279 -------------- src/View.zig | 271 ------------- src/VoidView.zig | 48 --- src/XdgPopup.zig | 82 ---- src/XdgToplevel.zig | 201 ---------- src/XwaylandUnmanaged.zig | 136 ------- src/XwaylandView.zig | 159 -------- src/c.zig | 60 --- src/command.zig | 122 ------ src/command/close.zig | 37 -- src/command/declare_mode.zig | 51 --- src/command/enter_mode.zig | 43 --- src/command/exit.zig | 34 -- src/command/focus.zig | 67 ---- src/command/focus_all_tags.zig | 32 -- src/command/focus_output.zig | 54 --- src/command/focus_tag.zig | 37 -- src/command/layout.zig | 37 -- src/command/map.zig | 110 ------ src/command/mod_master_count.zig | 42 -- src/command/mod_master_factor.zig | 45 --- src/command/send_to_output.zig | 62 --- src/command/spawn.zig | 47 --- src/command/tag_view.zig | 43 --- src/command/tag_view_all_tags.zig | 38 -- src/command/toggle_float.zig | 38 -- src/command/toggle_tag_focus.zig | 43 --- src/command/toggle_view_tag.zig | 44 --- src/command/zoom.zig | 54 --- src/log.zig | 41 -- src/render.zig | 315 --------------- src/river.zig | 42 -- src/riverctl.zig | 109 ------ src/test_main.zig | 20 - src/view_stack.zig | 406 -------------------- 97 files changed, 6102 insertions(+), 6102 deletions(-) create mode 100644 river/Box.zig create mode 100644 river/Config.zig create mode 100644 river/Control.zig create mode 100644 river/Cursor.zig create mode 100644 river/Decoration.zig create mode 100644 river/DecorationManager.zig create mode 100644 river/InputManager.zig create mode 100644 river/Keyboard.zig create mode 100644 river/LayerSurface.zig create mode 100644 river/Mapping.zig create mode 100644 river/Output.zig create mode 100644 river/Root.zig create mode 100644 river/Seat.zig create mode 100644 river/Server.zig create mode 100644 river/View.zig create mode 100644 river/VoidView.zig create mode 100644 river/XdgPopup.zig create mode 100644 river/XdgToplevel.zig create mode 100644 river/XwaylandUnmanaged.zig create mode 100644 river/XwaylandView.zig create mode 100644 river/c.zig create mode 100644 river/command.zig create mode 100644 river/command/close.zig create mode 100644 river/command/declare_mode.zig create mode 100644 river/command/enter_mode.zig create mode 100644 river/command/exit.zig create mode 100644 river/command/focus.zig create mode 100644 river/command/focus_all_tags.zig create mode 100644 river/command/focus_output.zig create mode 100644 river/command/focus_tag.zig create mode 100644 river/command/layout.zig create mode 100644 river/command/map.zig create mode 100644 river/command/mod_master_count.zig create mode 100644 river/command/mod_master_factor.zig create mode 100644 river/command/send_to_output.zig create mode 100644 river/command/spawn.zig create mode 100644 river/command/tag_view.zig create mode 100644 river/command/tag_view_all_tags.zig create mode 100644 river/command/toggle_float.zig create mode 100644 river/command/toggle_tag_focus.zig create mode 100644 river/command/toggle_view_tag.zig create mode 100644 river/command/zoom.zig create mode 100644 river/log.zig create mode 100644 river/main.zig create mode 100644 river/render.zig create mode 100644 river/test_main.zig create mode 100644 river/view_stack.zig create mode 100644 riverctl/main.zig delete mode 100644 src/Box.zig delete mode 100644 src/Config.zig delete mode 100644 src/Control.zig delete mode 100644 src/Cursor.zig delete mode 100644 src/Decoration.zig delete mode 100644 src/DecorationManager.zig delete mode 100644 src/InputManager.zig delete mode 100644 src/Keyboard.zig delete mode 100644 src/LayerSurface.zig delete mode 100644 src/Mapping.zig delete mode 100644 src/Output.zig delete mode 100644 src/Root.zig delete mode 100644 src/Seat.zig delete mode 100644 src/Server.zig delete mode 100644 src/View.zig delete mode 100644 src/VoidView.zig delete mode 100644 src/XdgPopup.zig delete mode 100644 src/XdgToplevel.zig delete mode 100644 src/XwaylandUnmanaged.zig delete mode 100644 src/XwaylandView.zig delete mode 100644 src/c.zig delete mode 100644 src/command.zig delete mode 100644 src/command/close.zig delete mode 100644 src/command/declare_mode.zig delete mode 100644 src/command/enter_mode.zig delete mode 100644 src/command/exit.zig delete mode 100644 src/command/focus.zig delete mode 100644 src/command/focus_all_tags.zig delete mode 100644 src/command/focus_output.zig delete mode 100644 src/command/focus_tag.zig delete mode 100644 src/command/layout.zig delete mode 100644 src/command/map.zig delete mode 100644 src/command/mod_master_count.zig delete mode 100644 src/command/mod_master_factor.zig delete mode 100644 src/command/send_to_output.zig delete mode 100644 src/command/spawn.zig delete mode 100644 src/command/tag_view.zig delete mode 100644 src/command/tag_view_all_tags.zig delete mode 100644 src/command/toggle_float.zig delete mode 100644 src/command/toggle_tag_focus.zig delete mode 100644 src/command/toggle_view_tag.zig delete mode 100644 src/command/zoom.zig delete mode 100644 src/log.zig delete mode 100644 src/render.zig delete mode 100644 src/river.zig delete mode 100644 src/riverctl.zig delete mode 100644 src/test_main.zig delete mode 100644 src/view_stack.zig diff --git a/build.zig b/build.zig index a9b05c5..5b86ce5 100644 --- a/build.zig +++ b/build.zig @@ -20,7 +20,7 @@ pub fn build(b: *std.build.Builder) !void { const scan_protocols = ScanProtocolsStep.create(b); { - const river = b.addExecutable("river", "src/river.zig"); + const river = b.addExecutable("river", "river/main.zig"); river.setTarget(target); river.setBuildMode(mode); river.addBuildOption(bool, "xwayland", xwayland); @@ -38,7 +38,7 @@ pub fn build(b: *std.build.Builder) !void { } { - const riverctl = b.addExecutable("riverctl", "src/riverctl.zig"); + const riverctl = b.addExecutable("riverctl", "riverctl/main.zig"); riverctl.setTarget(target); riverctl.setBuildMode(mode); @@ -52,7 +52,7 @@ pub fn build(b: *std.build.Builder) !void { } { - const river_test = b.addTest("src/test_main.zig"); + const river_test = b.addTest("river/test_main.zig"); river_test.setTarget(target); river_test.setBuildMode(mode); river_test.addBuildOption(bool, "xwayland", xwayland); diff --git a/river/Box.zig b/river/Box.zig new file mode 100644 index 0000000..0ba3e63 --- /dev/null +++ b/river/Box.zig @@ -0,0 +1,34 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const c = @import("c.zig"); + +x: i32, +y: i32, +width: u32, +height: u32, + +pub fn toWlrBox(self: Self) c.wlr_box { + return c.wlr_box{ + .x = @intCast(c_int, self.x), + .y = @intCast(c_int, self.y), + .width = @intCast(c_int, self.width), + .height = @intCast(c_int, self.height), + }; +} diff --git a/river/Config.zig b/river/Config.zig new file mode 100644 index 0000000..20b7504 --- /dev/null +++ b/river/Config.zig @@ -0,0 +1,284 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Server = @import("Server.zig"); +const Mapping = @import("Mapping.zig"); + +/// Width of borders in pixels +border_width: u32, + +/// Amount of view padding in pixels +view_padding: u32, + +/// Amount of padding arount the outer edge of the layout in pixels +outer_padding: u32, + +/// Map of keymap mode name to mode id +mode_to_id: std.StringHashMap(usize), + +/// All user-defined keymap modes, indexed by mode id +modes: std.ArrayList(std.ArrayList(Mapping)), + +/// List of app_ids which will be started floating +float_filter: std.ArrayList([*:0]const u8), + +pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { + self.border_width = 2; + self.view_padding = 8; + self.outer_padding = 8; + + self.mode_to_id = std.StringHashMap(usize).init(allocator); + try self.mode_to_id.putNoClobber("normal", 0); + try self.mode_to_id.putNoClobber("passthrough", 1); + + self.modes = std.ArrayList(std.ArrayList(Mapping)).init(allocator); + try self.modes.append(std.ArrayList(Mapping).init(allocator)); + try self.modes.append(std.ArrayList(Mapping).init(allocator)); + + self.float_filter = std.ArrayList([*:0]const u8).init(allocator); + + const normal_keybinds = &self.modes.items[0]; + const mod = c.WLR_MODIFIER_LOGO; + + // Mod+Shift+Return to start an instance of alacritty + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_Return, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "spawn", "alacritty" }, + )); + + // Mod+Q to close the focused view + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_q, + mod, + &[_][]const u8{"close"}, + )); + + // Mod+E to exit river + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_e, + mod, + &[_][]const u8{"exit"}, + )); + + // Mod+J and Mod+K to focus the next/previous view in the layout stack + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_j, + mod, + &[_][]const u8{ "focus", "next" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_k, + mod, + &[_][]const u8{ "focus", "previous" }, + )); + + // Mod+Return to bump the focused view to the top of the layout stack, + // making it the new master + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_Return, + mod, + &[_][]const u8{"zoom"}, + )); + + // Mod+H and Mod+L to increase/decrease the width of the master column + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_h, + mod, + &[_][]const u8{ "mod_master_factor", "+0.05" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_l, + mod, + &[_][]const u8{ "mod_master_factor", "-0.05" }, + )); + + // Mod+Shift+H and Mod+Shift+L to increment/decrement the number of + // master views in the layout + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_h, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "mod_master_count", "+1" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_l, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "mod_master_count", "+1" }, + )); + + comptime var i = 0; + inline while (i < 9) : (i += 1) { + const str = &[_]u8{i + '0' + 1}; + // Mod+[1-9] to focus tag [1-9] + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_1 + i, + mod, + &[_][]const u8{ "focus_tag", str }, + )); + // Mod+Shift+[1-9] to tag focused view with tag [1-9] + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_1 + i, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "tag_view", str }, + )); + // Mod+Ctrl+[1-9] to toggle focus of tag [1-9] + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_1 + i, + mod | c.WLR_MODIFIER_CTRL, + &[_][]const u8{ "toggle_tag_focus", str }, + )); + // Mod+Shift+Ctrl+[1-9] to toggle tag [1-9] of focused view + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_1 + i, + mod | c.WLR_MODIFIER_CTRL | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "toggle_view_tag", str }, + )); + } + + // Mod+0 to focus all tags + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_0, + mod, + &[_][]const u8{"focus_all_tags"}, + )); + + // Mod+Shift+0 to tag focused view with all tags + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_0, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{"tag_view_all_tags"}, + )); + + // Mod+Period and Mod+Comma to focus the next/previous output + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_period, + mod, + &[_][]const u8{ "focus_output", "next" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_comma, + mod, + &[_][]const u8{ "focus_output", "previous" }, + )); + + // Mod+Shift+Period/Comma to send the focused view to the the + // next/previous output + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_period, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "send_to_output", "next" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_comma, + mod | c.WLR_MODIFIER_SHIFT, + &[_][]const u8{ "send_to_output", "previous" }, + )); + + // Mod+Space to toggle float + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_space, + mod, + &[_][]const u8{"toggle_float"}, + )); + + // Mod+F11 to enter passthrough mode + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_F11, + mod, + &[_][]const u8{ "enter_mode", "passthrough" }, + )); + + // Change master orientation with Mod+{Up,Right,Down,Left} + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_Up, + mod, + &[_][]const u8{ "layout", "TopMaster" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_Right, + mod, + &[_][]const u8{ "layout", "RightMaster" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_Down, + mod, + &[_][]const u8{ "layout", "BottomMaster" }, + )); + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_Left, + mod, + &[_][]const u8{ "layout", "LeftMaster" }, + )); + + // Mod+f to change to Full layout + try normal_keybinds.append(try Mapping.init( + allocator, + c.XKB_KEY_f, + mod, + &[_][]const u8{ "layout", "Full" }, + )); + + // Mod+F11 to return to normal mode + try self.modes.items[1].append(try Mapping.init( + allocator, + c.XKB_KEY_F11, + mod, + &[_][]const u8{ "enter_mode", "normal" }, + )); + + // Float views with app_id "float" + try self.float_filter.append("float"); +} + +pub fn deinit(self: Self, allocator: *std.mem.Allocator) void { + self.mode_to_id.deinit(); + for (self.modes.items) |*mode| mode.deinit(); + self.modes.deinit(); +} diff --git a/river/Control.zig b/river/Control.zig new file mode 100644 index 0000000..e9ae576 --- /dev/null +++ b/river/Control.zig @@ -0,0 +1,139 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); +const command = @import("command.zig"); + +const Log = @import("log.zig").Log; +const Server = @import("Server.zig"); + +const protocol_version = 1; + +const implementation = c.struct_zriver_control_v1_interface{ + .run_command = runCommand, +}; + +server: *Server, +wl_global: *c.wl_global, + +listen_display_destroy: c.wl_listener, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + self.wl_global = c.wl_global_create( + server.wl_display, + &c.zriver_control_v1_interface, + protocol_version, + self, + bind, + ) orelse return error.CantCreateRiverWindowManagementGlobal; + + self.listen_display_destroy.notify = handleDisplayDestroy; + c.wl_display_add_destroy_listener(server.wl_display, &self.listen_display_destroy); +} + +fn handleDisplayDestroy(wl_listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_display_destroy", wl_listener.?); + c.wl_global_destroy(self.wl_global); +} + +/// Called when a client binds our global +fn bind(wl_client: ?*c.wl_client, data: ?*c_void, version: u32, id: u32) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); + const wl_resource = c.wl_resource_create( + wl_client, + &c.zriver_control_v1_interface, + @intCast(c_int, version), + id, + ) orelse { + c.wl_client_post_no_memory(wl_client); + return; + }; + c.wl_resource_set_implementation(wl_resource, &implementation, self, resourceDestroy); +} + +fn resourceDestroy(wl_resource: ?*c.wl_resource) callconv(.C) void { + // TODO +} + +fn runCommand( + wl_client: ?*c.wl_client, + wl_resource: ?*c.wl_resource, + wl_array: ?*c.wl_array, + callback_id: u32, +) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource))); + const allocator = self.server.allocator; + const seat = self.server.input_manager.default_seat; + + var args = std.ArrayList([]const u8).init(allocator); + + var i: usize = 0; + const data = @ptrCast([*]const u8, wl_array.?.data); + while (i < wl_array.?.size) { + const slice = std.mem.spanZ(@ptrCast([*:0]const u8, &data[i])); + args.append(std.mem.dupe(allocator, u8, slice) catch unreachable) catch unreachable; + + i += slice.len + 1; + } + + const callback_resource = c.wl_resource_create( + wl_client, + &c.zriver_command_callback_v1_interface, + protocol_version, + callback_id, + ) orelse { + c.wl_client_post_no_memory(wl_client); + return; + }; + + c.wl_resource_set_implementation(callback_resource, null, null, null); + + var failure_message: []const u8 = undefined; + command.run(allocator, seat, args.items, &failure_message) catch |err| { + if (err == command.Error.CommandFailed) { + defer allocator.free(failure_message); + const out = std.cstr.addNullByte(allocator, failure_message) catch { + c.zriver_command_callback_v1_send_failure(callback_resource, "out of memory"); + return; + }; + defer allocator.free(out); + c.zriver_command_callback_v1_send_failure(callback_resource, out); + } else { + c.zriver_command_callback_v1_send_failure( + callback_resource, + switch (err) { + command.Error.NoCommand => "no command given", + command.Error.UnknownCommand => "unknown command", + command.Error.NotEnoughArguments => "not enough arguments", + command.Error.TooManyArguments => "too many arguments", + command.Error.Overflow => "value out of bounds", + command.Error.InvalidCharacter => "invalid character in argument", + command.Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'", + command.Error.OutOfMemory => "out of memory", + command.Error.CommandFailed => unreachable, + }, + ); + } + return; + }; + c.zriver_command_callback_v1_send_success(callback_resource); +} diff --git a/river/Cursor.zig b/river/Cursor.zig new file mode 100644 index 0000000..9eede57 --- /dev/null +++ b/river/Cursor.zig @@ -0,0 +1,412 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const build_options = @import("build_options"); +const std = @import("std"); + +const c = @import("c.zig"); + +const LayerSurface = @import("LayerSurface.zig"); +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const Seat = @import("Seat.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +const CursorMode = enum { + Passthrough, + Move, + Resize, +}; + +seat: *Seat, +wlr_cursor: *c.wlr_cursor, +wlr_xcursor_manager: *c.wlr_xcursor_manager, + +mode: CursorMode, +grabbed_view: ?*View, +grab_x: f64, +grab_y: f64, +grab_width: c_int, +grab_height: c_int, +resize_edges: u32, + +listen_axis: c.wl_listener, +listen_button: c.wl_listener, +listen_frame: c.wl_listener, +listen_motion_absolute: c.wl_listener, +listen_motion: c.wl_listener, +listen_request_set_cursor: c.wl_listener, + +pub fn init(self: *Self, seat: *Seat) !void { + self.seat = seat; + + // Creates a wlroots utility for tracking the cursor image shown on screen. + self.wlr_cursor = c.wlr_cursor_create() orelse + return error.CantCreateWlrCursor; + + // Creates an xcursor manager, another wlroots utility which loads up + // Xcursor themes to source cursor images from and makes sure that cursor + // images are available at all scale factors on the screen (necessary for + // HiDPI support). We add a cursor theme at scale factor 1 to begin with. + self.wlr_xcursor_manager = c.wlr_xcursor_manager_create(null, 24) orelse + return error.CantCreateWlrXCursorManager; + c.wlr_cursor_attach_output_layout(self.wlr_cursor, seat.input_manager.server.root.wlr_output_layout); + if (c.wlr_xcursor_manager_load(self.wlr_xcursor_manager, 1) == 0) { + if (build_options.xwayland) { + if (c.wlr_xcursor_manager_get_xcursor( + self.wlr_xcursor_manager, + "left_ptr", + 1, + )) |wlr_xcursor| { + const image: *c.wlr_xcursor_image = wlr_xcursor.*.images[0]; + c.wlr_xwayland_set_cursor( + seat.input_manager.server.wlr_xwayland, + image.buffer, + image.width * 4, + image.width, + image.height, + @intCast(i32, image.hotspot_x), + @intCast(i32, image.hotspot_y), + ); + } + } + } else { + Log.Error.log("Failed to load an xcursor theme", .{}); + } + + self.mode = CursorMode.Passthrough; + self.grabbed_view = null; + self.grab_x = 0.0; + self.grab_y = 0.0; + self.grab_width = 0; + self.grab_height = 0; + self.resize_edges = 0; + + // wlr_cursor *only* displays an image on screen. It does not move around + // when the pointer moves. However, we can attach input devices to it, and + // it will generate aggregate events for all of them. In these events, we + // can choose how we want to process them, forwarding them to clients and + // moving the cursor around. See following post for more detail: + // https://drewdevault.com/2018/07/17/Input-handling-in-wlroots.html + self.listen_axis.notify = handleAxis; + c.wl_signal_add(&self.wlr_cursor.events.axis, &self.listen_axis); + + self.listen_button.notify = handleButton; + c.wl_signal_add(&self.wlr_cursor.events.button, &self.listen_button); + + self.listen_frame.notify = handleFrame; + c.wl_signal_add(&self.wlr_cursor.events.frame, &self.listen_frame); + + self.listen_motion_absolute.notify = handleMotionAbsolute; + c.wl_signal_add(&self.wlr_cursor.events.motion_absolute, &self.listen_motion_absolute); + + self.listen_motion.notify = handleMotion; + c.wl_signal_add(&self.wlr_cursor.events.motion, &self.listen_motion); + + self.listen_request_set_cursor.notify = handleRequestSetCursor; + c.wl_signal_add(&self.seat.wlr_seat.events.request_set_cursor, &self.listen_request_set_cursor); +} + +pub fn deinit(self: *Self) void { + c.wlr_xcursor_manager_destroy(self.wlr_xcursor_manager); + c.wlr_cursor_destroy(self.wlr_cursor); +} + +fn handleAxis(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is forwarded by the cursor when a pointer emits an axis event, + // for example when you move the scroll wheel. + const cursor = @fieldParentPtr(Self, "listen_axis", listener.?); + const event = @ptrCast( + *c.wlr_event_pointer_axis, + @alignCast(@alignOf(*c.wlr_event_pointer_axis), data), + ); + + // Notify the client with pointer focus of the axis event. + c.wlr_seat_pointer_notify_axis( + cursor.seat.wlr_seat, + event.time_msec, + event.orientation, + event.delta, + event.delta_discrete, + event.source, + ); +} + +fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is forwarded by the cursor when a pointer emits a button + // event. + const self = @fieldParentPtr(Self, "listen_button", listener.?); + const event = @ptrCast( + *c.wlr_event_pointer_button, + @alignCast(@alignOf(*c.wlr_event_pointer_button), data), + ); + var sx: f64 = undefined; + var sy: f64 = undefined; + + if (self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy)) |wlr_surface| { + // If the found surface is a keyboard inteactive layer surface, + // give it keyboard focus. + if (c.wlr_surface_is_layer_surface(wlr_surface)) { + const wlr_layer_surface = c.wlr_layer_surface_v1_from_wlr_surface(wlr_surface); + if (wlr_layer_surface.*.current.keyboard_interactive) { + const layer_surface = @ptrCast( + *LayerSurface, + @alignCast(@alignOf(*LayerSurface), wlr_layer_surface.*.data), + ); + self.seat.setFocusRaw(.{ .layer = layer_surface }); + } + } + + // If the found surface is an xdg toplevel surface, send keyboard + // focus to the view. + if (c.wlr_surface_is_xdg_surface(wlr_surface)) { + const wlr_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(wlr_surface); + if (wlr_xdg_surface.*.role == .WLR_XDG_SURFACE_ROLE_TOPLEVEL) { + const view = @ptrCast(*View, @alignCast(@alignOf(*View), wlr_xdg_surface.*.data)); + self.seat.focus(view); + } + } + + _ = c.wlr_seat_pointer_notify_button( + self.seat.wlr_seat, + event.time_msec, + event.button, + event.state, + ); + } +} + +fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is forwarded by the cursor when a pointer emits an frame + // event. Frame events are sent after regular pointer events to group + // multiple events together. For instance, two axis events may happen at the + // same time, in which case a frame event won't be sent in between. + const self = @fieldParentPtr(Self, "listen_frame", listener.?); + // Notify the client with pointer focus of the frame event. + c.wlr_seat_pointer_notify_frame(self.seat.wlr_seat); +} + +fn handleMotionAbsolute(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is forwarded by the cursor when a pointer emits an _absolute_ + // motion event, from 0..1 on each axis. This happens, for example, when + // wlroots is running under a Wayland window rather than KMS+DRM, and you + // move the mouse over the window. You could enter the window from any edge, + // so we have to warp the mouse there. There is also some hardware which + // emits these events. + const self = @fieldParentPtr(Self, "listen_motion_absolute", listener.?); + const event = @ptrCast( + *c.wlr_event_pointer_motion_absolute, + @alignCast(@alignOf(*c.wlr_event_pointer_motion_absolute), data), + ); + c.wlr_cursor_warp_absolute(self.wlr_cursor, event.device, event.x, event.y); + self.processMotion(event.time_msec); +} + +fn handleMotion(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is forwarded by the cursor when a pointer emits a _relative_ + // pointer motion event (i.e. a delta) + const self = @fieldParentPtr(Self, "listen_motion", listener.?); + const event = @ptrCast( + *c.wlr_event_pointer_motion, + @alignCast(@alignOf(*c.wlr_event_pointer_motion), data), + ); + // The cursor doesn't move unless we tell it to. The cursor automatically + // handles constraining the motion to the output layout, as well as any + // special configuration applied for the specific input device which + // generated the event. You can pass NULL for the device if you want to move + // the cursor around without any input. + c.wlr_cursor_move(self.wlr_cursor, event.device, event.delta_x, event.delta_y); + self.processMotion(event.time_msec); +} + +fn handleRequestSetCursor(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is rasied by the seat when a client provides a cursor image + const self = @fieldParentPtr(Self, "listen_request_set_cursor", listener.?); + const event = @ptrCast( + *c.wlr_seat_pointer_request_set_cursor_event, + @alignCast(@alignOf(*c.wlr_seat_pointer_request_set_cursor_event), data), + ); + const focused_client = self.seat.wlr_seat.pointer_state.focused_client; + + // This can be sent by any client, so we check to make sure this one is + // actually has pointer focus first. + if (focused_client == event.seat_client) { + // Once we've vetted the client, we can tell the cursor to use the + // provided surface as the cursor image. It will set the hardware cursor + // on the output that it's currently on and continue to do so as the + // cursor moves between outputs. + Log.Debug.log("Focused client set cursor", .{}); + c.wlr_cursor_set_surface( + self.wlr_cursor, + event.surface, + event.hotspot_x, + event.hotspot_y, + ); + } +} + +fn processMotion(self: Self, time: u32) void { + var sx: f64 = undefined; + var sy: f64 = undefined; + if (self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy)) |wlr_surface| { + // "Enter" the surface if necessary. This lets the client know that the + // cursor has entered one of its surfaces. + // + // Note that this gives the surface "pointer focus", which is distinct + // from keyboard focus. You get pointer focus by moving the pointer over + // a window. + if (self.seat.input_manager.inputAllowed(wlr_surface)) { + const wlr_seat = self.seat.wlr_seat; + const focus_change = wlr_seat.pointer_state.focused_surface != wlr_surface; + if (focus_change) { + Log.Debug.log("Pointer notify enter at ({},{})", .{ sx, sy }); + c.wlr_seat_pointer_notify_enter(wlr_seat, wlr_surface, sx, sy); + } else { + // The enter event contains coordinates, so we only need to notify + // on motion if the focus did not change. + c.wlr_seat_pointer_notify_motion(wlr_seat, time, sx, sy); + } + return; + } + } + + // There is either no surface under the cursor or input is disallowed + // Reset the cursor image to the default + c.wlr_xcursor_manager_set_cursor_image( + self.wlr_xcursor_manager, + "left_ptr", + self.wlr_cursor, + ); + // Clear pointer focus so future button events and such are not sent to + // the last client to have the cursor over it. + c.wlr_seat_pointer_clear_focus(self.seat.wlr_seat); +} + +/// Find the topmost surface under the output layout coordinates lx/ly +/// returns the surface if found and sets the sx/sy parametes to the +/// surface coordinates. +fn surfaceAt(self: Self, lx: f64, ly: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + // Find the output to check + const root = self.seat.input_manager.server.root; + const wlr_output = c.wlr_output_layout_output_at(root.wlr_output_layout, lx, ly) orelse + return null; + const output = @ptrCast( + *Output, + @alignCast(@alignOf(*Output), wlr_output.*.data orelse return null), + ); + + // Get output-local coords from the layout coords + var ox = lx; + var oy = ly; + c.wlr_output_layout_output_coords(root.wlr_output_layout, wlr_output, &ox, &oy); + + // Check layers and views from top to bottom + const layer_idxs = [_]usize{ + c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + c.ZWLR_LAYER_SHELL_V1_LAYER_TOP, + c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, + c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, + }; + + // Check overlay layer incl. popups + if (layerSurfaceAt(output.*, output.layers[layer_idxs[0]], ox, oy, sx, sy, false)) |surface| { + return surface; + } + + // Check top-background popups only + for (layer_idxs[1..4]) |layer_idx| { + if (layerSurfaceAt(output.*, output.layers[layer_idx], ox, oy, sx, sy, true)) |surface| { + return surface; + } + } + + // Check top layer + if (layerSurfaceAt(output.*, output.layers[layer_idxs[1]], ox, oy, sx, sy, false)) |surface| { + return surface; + } + + // Check floating views then normal views + if (viewSurfaceAt(output.*, ox, oy, sx, sy, true)) |surface| { + return surface; + } + if (viewSurfaceAt(output.*, ox, oy, sx, sy, false)) |surface| { + return surface; + } + + // Check the bottom-background layers + for (layer_idxs[2..4]) |layer_idx| { + if (layerSurfaceAt(output.*, output.layers[layer_idx], ox, oy, sx, sy, false)) |surface| { + return surface; + } + } + + return null; +} + +/// Find the topmost surface on the given layer at ox,oy. Will only check +/// popups if popups_only is true. +fn layerSurfaceAt( + output: Output, + layer: std.TailQueue(LayerSurface), + ox: f64, + oy: f64, + sx: *f64, + sy: *f64, + popups_only: bool, +) ?*c.wlr_surface { + var it = layer.first; + while (it) |node| : (it = node.next) { + const layer_surface = &node.data; + const surface = c.wlr_layer_surface_v1_surface_at( + layer_surface.wlr_layer_surface, + ox - @intToFloat(f64, layer_surface.box.x), + oy - @intToFloat(f64, layer_surface.box.y), + sx, + sy, + ); + if (surface) |found| { + if (!popups_only) { + return found; + } else if (c.wlr_surface_is_xdg_surface(found)) { + const wlr_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(found); + if (wlr_xdg_surface.*.role == .WLR_XDG_SURFACE_ROLE_POPUP) { + return found; + } + } + } + } + return null; +} + +/// Find the topmost visible view surface (incl. popups) at ox,oy. Will +/// check only floating views if floating is true. +fn viewSurfaceAt(output: Output, ox: f64, oy: f64, sx: *f64, sy: *f64, floating: bool) ?*c.wlr_surface { + var it = ViewStack(View).iterator(output.views.first, output.current_focused_tags); + while (it.next()) |node| { + const view = &node.view; + if (view.floating != floating) { + continue; + } + if (view.surfaceAt(ox, oy, sx, sy)) |found| { + return found; + } + } + return null; +} diff --git a/river/Decoration.zig b/river/Decoration.zig new file mode 100644 index 0000000..91c5bed --- /dev/null +++ b/river/Decoration.zig @@ -0,0 +1,53 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const DecorationManager = @import("DecorationManager.zig"); + +// TODO: this needs to listen for destroy and free nodes from the deco list +decoration_manager: *DecorationManager, +wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, + +listen_request_mode: c.wl_listener, + +pub fn init( + self: *Self, + decoration_manager: *DecorationManager, + wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, +) void { + self.decoration_manager = decoration_manager; + self.wlr_xdg_toplevel_decoration = wlr_xdg_toplevel_decoration; + + self.listen_request_mode.notify = handleRequestMode; + c.wl_signal_add(&self.wlr_xdg_toplevel_decoration.events.request_mode, &self.listen_request_mode); + + handleRequestMode(&self.listen_request_mode, self.wlr_xdg_toplevel_decoration); +} + +fn handleRequestMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_request_mode", listener.?); + // TODO: we might need to take this configure serial and do a transaction + _ = c.wlr_xdg_toplevel_decoration_v1_set_mode( + self.wlr_xdg_toplevel_decoration, + c.wlr_xdg_toplevel_decoration_v1_mode.WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE, + ); +} diff --git a/river/DecorationManager.zig b/river/DecorationManager.zig new file mode 100644 index 0000000..5a9abb4 --- /dev/null +++ b/river/DecorationManager.zig @@ -0,0 +1,57 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Decoration = @import("Decoration.zig"); +const Server = @import("Server.zig"); + +server: *Server, + +wlr_xdg_decoration_manager: *c.wlr_xdg_decoration_manager_v1, + +decorations: std.SinglyLinkedList(Decoration), + +listen_new_toplevel_decoration: c.wl_listener, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + self.wlr_xdg_decoration_manager = c.wlr_xdg_decoration_manager_v1_create(server.wl_display) orelse + return error.CantCreateWlrXdgDecorationManager; + + self.listen_new_toplevel_decoration.notify = handleNewToplevelDecoration; + c.wl_signal_add( + &self.wlr_xdg_decoration_manager.events.new_toplevel_decoration, + &self.listen_new_toplevel_decoration, + ); +} + +fn handleNewToplevelDecoration(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_toplevel_decoration", listener.?); + const wlr_xdg_toplevel_decoration = @ptrCast( + *c.wlr_xdg_toplevel_decoration_v1, + @alignCast(@alignOf(*c.wlr_xdg_toplevel_decoration_v1), data), + ); + + const node = self.decorations.allocateNode(self.server.allocator) catch unreachable; + node.data.init(self, wlr_xdg_toplevel_decoration); + self.decorations.prepend(node); +} diff --git a/river/InputManager.zig b/river/InputManager.zig new file mode 100644 index 0000000..3a95346 --- /dev/null +++ b/river/InputManager.zig @@ -0,0 +1,146 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Seat = @import("Seat.zig"); +const Server = @import("Server.zig"); + +const default_seat_name = "default"; + +server: *Server, + +wlr_input_inhibit_manager: *c.wlr_input_inhibit_manager, + +seats: std.TailQueue(Seat), +default_seat: *Seat, + +exclusive_client: ?*c.wl_client, + +listen_inhibit_activate: c.wl_listener, +listen_inhibit_deactivate: c.wl_listener, +listen_new_input: c.wl_listener, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + + // This is automatically freed when the display is destroyed + self.wlr_input_inhibit_manager = + c.wlr_input_inhibit_manager_create(server.wl_display) orelse + return error.CantCreateInputInhibitManager; + + self.seats = std.TailQueue(Seat).init(); + + const seat_node = try server.allocator.create(std.TailQueue(Seat).Node); + try seat_node.data.init(self, default_seat_name); + self.default_seat = &seat_node.data; + self.seats.prepend(seat_node); + + self.exclusive_client = null; + + // Set up all listeners + self.listen_inhibit_activate.notify = handleInhibitActivate; + c.wl_signal_add( + &self.wlr_input_inhibit_manager.events.activate, + &self.listen_inhibit_activate, + ); + + self.listen_inhibit_deactivate.notify = handleInhibitDeactivate; + c.wl_signal_add( + &self.wlr_input_inhibit_manager.events.deactivate, + &self.listen_inhibit_deactivate, + ); + + self.listen_new_input.notify = handleNewInput; + c.wl_signal_add(&self.server.wlr_backend.events.new_input, &self.listen_new_input); +} + +pub fn deinit(self: *Self) void { + while (self.seats.pop()) |seat_node| { + seat_node.data.deinit(); + self.server.allocator.destroy(seat_node); + } +} + +/// Must be called whenever a view is unmapped. +pub fn handleViewUnmap(self: Self, view: *View) void { + var it = self.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + seat.handleViewUnmap(view); + } +} + +/// Returns true if input is currently allowed on the passed surface. +pub fn inputAllowed(self: Self, wlr_surface: *c.wlr_surface) bool { + return if (self.exclusive_client) |exclusive_client| + exclusive_client == c.wl_resource_get_client(wlr_surface.resource) + else + true; +} + +fn handleInhibitActivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_inhibit_activate", listener.?); + + Log.Debug.log("Input inhibitor activated", .{}); + + // Clear focus of all seats + var seat_it = self.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + seat_node.data.setFocusRaw(.{ .none = {} }); + } + + self.exclusive_client = self.wlr_input_inhibit_manager.active_client; +} + +fn handleInhibitDeactivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_inhibit_deactivate", listener.?); + + Log.Debug.log("Input inhibitor deactivated", .{}); + + self.exclusive_client = null; + + // Calling arrangeLayers() like this ensures that any top or overlay, + // keyboard-interactive surfaces will re-grab focus. + var output_it = self.server.root.outputs.first; + while (output_it) |output_node| : (output_it = output_node.next) { + output_node.data.arrangeLayers(); + } + + // After ensuring that any possible layer surface focus grab has occured, + // have each Seat handle focus. + var seat_it = self.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + seat_node.data.focus(null); + } +} + +/// This event is raised by the backend when a new input device becomes available. +fn handleNewInput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_input", listener.?); + const device = @ptrCast(*c.wlr_input_device, @alignCast(@alignOf(*c.wlr_input_device), data)); + + // TODO: suport multiple seats + if (self.seats.first) |seat_node| { + seat_node.data.addDevice(device) catch unreachable; + } +} diff --git a/river/Keyboard.zig b/river/Keyboard.zig new file mode 100644 index 0000000..cbfd6cd --- /dev/null +++ b/river/Keyboard.zig @@ -0,0 +1,180 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Seat = @import("Seat.zig"); + +seat: *Seat, +wlr_input_device: *c.wlr_input_device, +wlr_keyboard: *c.wlr_keyboard, + +listen_key: c.wl_listener, +listen_modifiers: c.wl_listener, + +pub fn init(self: *Self, seat: *Seat, wlr_input_device: *c.wlr_input_device) !void { + self.seat = seat; + self.wlr_input_device = wlr_input_device; + self.wlr_keyboard = @field(wlr_input_device, c.wlr_input_device_union).keyboard; + + // We need to prepare an XKB keymap and assign it to the keyboard. This + // assumes the defaults (e.g. layout = "us"). + const rules = c.xkb_rule_names{ + .rules = null, + .model = null, + .layout = null, + .variant = null, + .options = null, + }; + const context = c.xkb_context_new(.XKB_CONTEXT_NO_FLAGS) orelse + return error.CantCreateXkbContext; + defer c.xkb_context_unref(context); + + const keymap = c.xkb_keymap_new_from_names( + context, + &rules, + .XKB_KEYMAP_COMPILE_NO_FLAGS, + ) orelse + return error.CantCreateXkbKeymap; + defer c.xkb_keymap_unref(keymap); + + // TODO: handle failure after https://github.com/swaywm/wlroots/pull/2081 + c.wlr_keyboard_set_keymap(self.wlr_keyboard, keymap); + c.wlr_keyboard_set_repeat_info(self.wlr_keyboard, 25, 600); + + // Setup listeners for keyboard events + self.listen_key.notify = handleKey; + c.wl_signal_add(&self.wlr_keyboard.events.key, &self.listen_key); + + self.listen_modifiers.notify = handleModifiers; + c.wl_signal_add(&self.wlr_keyboard.events.modifiers, &self.listen_modifiers); +} + +fn handleKey(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when a key is pressed or released. + const self = @fieldParentPtr(Self, "listen_key", listener.?); + const event = @ptrCast( + *c.wlr_event_keyboard_key, + @alignCast(@alignOf(*c.wlr_event_keyboard_key), data), + ); + + const wlr_keyboard = self.wlr_keyboard; + + // Translate libinput keycode -> xkbcommon + const keycode = event.keycode + 8; + + // Get a list of keysyms as xkb reports them + var translated_keysyms: ?[*]c.xkb_keysym_t = undefined; + const translated_keysyms_len = c.xkb_state_key_get_syms( + wlr_keyboard.xkb_state, + keycode, + &translated_keysyms, + ); + + // Get a list of keysyms ignoring modifiers (e.g. 1 instead of !) + // Important for mappings like Mod+Shift+1 + var raw_keysyms: ?[*]c.xkb_keysym_t = undefined; + const layout_index = c.xkb_state_key_get_layout(wlr_keyboard.xkb_state, keycode); + const raw_keysyms_len = c.xkb_keymap_key_get_syms_by_level( + wlr_keyboard.keymap, + keycode, + layout_index, + 0, + &raw_keysyms, + ); + + var handled = false; + // TODO: These modifiers aren't properly handled, see sway's code + const modifiers = c.wlr_keyboard_get_modifiers(wlr_keyboard); + if (event.state == .WLR_KEY_PRESSED) { + var i: usize = 0; + while (i < translated_keysyms_len) : (i += 1) { + if (self.handleBuiltinMapping(translated_keysyms.?[i])) { + handled = true; + break; + } else if (self.seat.handleMapping(translated_keysyms.?[i], modifiers)) { + handled = true; + break; + } + } + if (!handled) { + i = 0; + while (i < raw_keysyms_len) : (i += 1) { + if (self.handleBuiltinMapping(raw_keysyms.?[i])) { + handled = true; + break; + } else if (self.seat.handleMapping(raw_keysyms.?[i], modifiers)) { + handled = true; + break; + } + } + } + } + + if (!handled) { + // Otherwise, we pass it along to the client. + const wlr_seat = self.seat.wlr_seat; + c.wlr_seat_set_keyboard(wlr_seat, self.wlr_input_device); + c.wlr_seat_keyboard_notify_key( + wlr_seat, + event.time_msec, + event.keycode, + @intCast(u32, @enumToInt(event.state)), + ); + } +} + +fn handleModifiers(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when a modifier key, such as shift or alt, is + // pressed. We simply communicate this to the client. */ + const self = @fieldParentPtr(Self, "listen_modifiers", listener.?); + + // A seat can only have one keyboard, but this is a limitation of the + // Wayland protocol - not wlroots. We assign all connected keyboards to the + // same seat. You can swap out the underlying wlr_keyboard like this and + // wlr_seat handles this transparently. + c.wlr_seat_set_keyboard(self.seat.wlr_seat, self.wlr_input_device); + + // Send modifiers to the client. + c.wlr_seat_keyboard_notify_modifiers( + self.seat.wlr_seat, + &self.wlr_keyboard.modifiers, + ); +} + +/// Handle any builtin, harcoded compsitor mappings such as VT switching. +/// Returns true if the keysym was handled. +fn handleBuiltinMapping(self: Self, keysym: c.xkb_keysym_t) bool { + if (keysym >= c.XKB_KEY_XF86Switch_VT_1 and keysym <= c.XKB_KEY_XF86Switch_VT_12) { + Log.Debug.log("Switch VT keysym received", .{}); + const wlr_backend = self.seat.input_manager.server.wlr_backend; + if (c.river_wlr_backend_is_multi(wlr_backend)) { + if (c.river_wlr_backend_get_session(wlr_backend)) |session| { + const vt = keysym - c.XKB_KEY_XF86Switch_VT_1 + 1; + Log.Debug.log("Switching to VT {}", .{vt}); + _ = c.wlr_session_change_vt(session, vt); + } + } + return true; + } + return false; +} diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig new file mode 100644 index 0000000..088dd97 --- /dev/null +++ b/river/LayerSurface.zig @@ -0,0 +1,196 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const XdgPopup = @import("XdgPopup.zig"); + +output: *Output, +wlr_layer_surface: *c.wlr_layer_surface_v1, + +box: Box, +layer: c.zwlr_layer_shell_v1_layer, + +// Listeners active the entire lifetime of the layser surface +listen_destroy: c.wl_listener, +listen_map: c.wl_listener, +listen_unmap: c.wl_listener, + +// Listeners only active while the layer surface is mapped +listen_commit: c.wl_listener, +listen_new_popup: c.wl_listener, + +pub fn init( + self: *Self, + output: *Output, + wlr_layer_surface: *c.wlr_layer_surface_v1, +) void { + self.output = output; + self.wlr_layer_surface = wlr_layer_surface; + wlr_layer_surface.data = self; + + self.layer = wlr_layer_surface.client_pending.layer; + + // Temporarily add to the output's list and apply the pending state to allow + // for inital arrangement which sends the first configure. + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + const list = &output.layers[@intCast(usize, @enumToInt(self.layer))]; + const stashed_state = wlr_layer_surface.current; + wlr_layer_surface.current = wlr_layer_surface.client_pending; + list.append(node); + output.arrangeLayers(); + list.remove(node); + wlr_layer_surface.current = stashed_state; + + // Set up listeners that are active for the entire lifetime of the layer surface + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&self.wlr_layer_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&self.wlr_layer_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&self.wlr_layer_surface.events.unmap, &self.listen_unmap); +} + +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + const output = self.output; + + Log.Debug.log("Layer surface '{}' destroyed", .{self.wlr_layer_surface.namespace}); + + // Remove listeners active the entire lifetime of the layer surface + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_map.link); + c.wl_list_remove(&self.listen_unmap.link); + + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + output.root.server.allocator.destroy(node); +} + +fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_map", listener.?); + const wlr_layer_surface = self.wlr_layer_surface; + + Log.Debug.log("Layer surface '{}' mapped.", .{wlr_layer_surface.namespace}); + + // Add listeners that are only active while mapped + self.listen_commit.notify = handleCommit; + c.wl_signal_add(&wlr_layer_surface.surface.*.events.commit, &self.listen_commit); + + self.listen_new_popup.notify = handleNewPopup; + c.wl_signal_add(&wlr_layer_surface.events.new_popup, &self.listen_new_popup); + + c.wlr_surface_send_enter( + wlr_layer_surface.surface, + wlr_layer_surface.output, + ); + + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.output.layers[@intCast(usize, @enumToInt(self.layer))].append(node); +} + +fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_unmap", listener.?); + + Log.Debug.log("Layer surface '{}' unmapped.", .{self.wlr_layer_surface.namespace}); + + // This is a bit ugly: we need to use the wlr bool here since surfaces + // may be closed during the inital configure which we preform + // while unmapped. wlroots currently calls unmap unconditionally on close + // even if the surface is not mapped. I sent a patch which was merged, but + // we need to wait for a release to use it. + // + // TODO(wlroots): Remove this check on updating + // https://github.com/swaywm/wlroots/commit/11e94c406bb75c9a8990ce99489798411deb110c + if (self.wlr_layer_surface.mapped) { + // remove listeners only active while the layer surface is mapped + c.wl_list_remove(&self.listen_commit.link); + c.wl_list_remove(&self.listen_new_popup.link); + } + + // Remove from the output's list of layer surfaces + const self_node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.output.layers[@intCast(usize, @enumToInt(self.layer))].remove(self_node); + + // If the unmapped surface is focused, clear focus + var it = self.output.root.server.input_manager.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + if (seat.focused_layer) |current_focus| { + if (current_focus == self) { + seat.setFocusRaw(.{ .none = {} }); + } + } + } + + // This gives exclusive focus to a keyboard interactive top or overlay layer + // surface if there is one. + self.output.arrangeLayers(); + + // Ensure that focus is given to the appropriate view if there is no + // other top/overlay layer surface to grab focus. + it = self.output.root.server.input_manager.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + seat.focus(null); + } +} + +fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_commit", listener.?); + const wlr_layer_surface = self.wlr_layer_surface; + + if (self.wlr_layer_surface.output == null) { + Log.Error.log("Layer surface committed with null output", .{}); + return; + } + + // If the layer changed, move the LayerSurface to the proper list + if (self.layer != self.wlr_layer_surface.current.layer) { + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + + const old_layer_idx = @intCast(usize, @enumToInt(self.layer)); + self.output.layers[old_layer_idx].remove(node); + + self.layer = self.wlr_layer_surface.current.layer; + + const new_layer_idx = @intCast(usize, @enumToInt(self.layer)); + self.output.layers[new_layer_idx].append(node); + } + + // TODO: only reconfigure if things haven't changed + // https://github.com/swaywm/wlroots/issues/1079 + self.output.arrangeLayers(); +} + +fn handleNewPopup(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_popup", listener.?); + const wlr_xdg_popup = @ptrCast(*c.wlr_xdg_popup, @alignCast(@alignOf(*c.wlr_xdg_popup), data)); + const allocator = self.output.root.server.allocator; + + // This will free itself on destroy + var xdg_popup = allocator.create(XdgPopup) catch unreachable; + xdg_popup.init(self.output, &self.box, wlr_xdg_popup); +} diff --git a/river/Mapping.zig b/river/Mapping.zig new file mode 100644 index 0000000..61df624 --- /dev/null +++ b/river/Mapping.zig @@ -0,0 +1,46 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +keysym: c.xkb_keysym_t, +modifiers: u32, +command_args: []const []const u8, + +pub fn init( + allocator: *std.mem.Allocator, + keysym: c.xkb_keysym_t, + modifiers: u32, + command_args: []const []const u8, +) !Self { + var owned_args = try allocator.alloc([]u8, command_args.len); + for (command_args) |arg, i| owned_args[i] = try std.mem.dupe(allocator, u8, arg); + return Self{ + .keysym = keysym, + .modifiers = modifiers, + .command_args = owned_args, + }; +} + +pub fn deinit(self: Self, allocator: *std.mem.Allocator) void { + for (self.command_args) |arg| allocator.free(arg); + allocator.free(self.command_args); +} diff --git a/river/Output.zig b/river/Output.zig new file mode 100644 index 0000000..a32c000 --- /dev/null +++ b/river/Output.zig @@ -0,0 +1,744 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); +const render = @import("render.zig"); + +const Box = @import("Box.zig"); +const LayerSurface = @import("LayerSurface.zig"); +const Log = @import("log.zig").Log; +const Root = @import("Root.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +root: *Root, +wlr_output: *c.wlr_output, + +/// All layer surfaces on the output, indexed by the layer enum. +layers: [4]std.TailQueue(LayerSurface), + +/// The area left for views and other layer surfaces after applying the +/// exclusive zones of exclusive layer surfaces. +usable_box: Box, + +/// The top of the stack is the "most important" view. +views: ViewStack(View), + +/// A bit field of focused tags +current_focused_tags: u32, +pending_focused_tags: ?u32, + +/// Number of views in "master" section of the screen. +master_count: u32, + +/// Percentage of the total screen that the master section takes up. +master_factor: f64, + +/// Current layout of the output. +layout: Layout, + +// All listeners for this output, in alphabetical order +listen_destroy: c.wl_listener, +listen_frame: c.wl_listener, +listen_mode: c.wl_listener, + +// All possible layouts. +pub const Layout = enum { + TopMaster, + RightMaster, + BottomMaster, + LeftMaster, + Full, +}; + +const LayoutName = struct { + name: []const u8, + layout: Layout, +}; + +// zig fmt: off +const layout_names = [_]LayoutName { + .{ .name = "TopMaster", .layout = Layout.TopMaster, }, + .{ .name = "RightMaster", .layout = Layout.RightMaster, }, + .{ .name = "BottomMaster", .layout = Layout.BottomMaster, }, + .{ .name = "LeftMaster", .layout = Layout.LeftMaster, }, + .{ .name = "Full", .layout = Layout.Full, }, +}; +// zig fmt: on + +pub fn getLayoutByName(self: Self, name: []const u8) Layout { + for (layout_names) |current| { + if (std.mem.eql(u8, name, current.name)) { + return current.layout; + } + } + Log.Error.log("Layout '{}' does not exist", .{name}); + // In case of error default to LeftMaster + return Layout.LeftMaster; +} + +pub fn init(self: *Self, root: *Root, wlr_output: *c.wlr_output) !void { + // Some backends don't have modes. DRM+KMS does, and we need to set a mode + // before we can use the output. The mode is a tuple of (width, height, + // refresh rate), and each monitor supports only a specific set of modes. We + // just pick the monitor's preferred mode, a more sophisticated compositor + // would let the user configure it. + + // if not empty + if (c.wl_list_empty(&wlr_output.modes) == 0) { + // TODO: handle failure + const mode = c.wlr_output_preferred_mode(wlr_output); + c.wlr_output_set_mode(wlr_output, mode); + c.wlr_output_enable(wlr_output, true); + if (!c.wlr_output_commit(wlr_output)) { + return error.CantCommitWlrOutputMode; + } + } + + self.root = root; + self.wlr_output = wlr_output; + wlr_output.data = self; + + for (self.layers) |*layer| { + layer.* = std.TailQueue(LayerSurface).init(); + } + + self.views.init(); + + self.current_focused_tags = 1 << 0; + self.pending_focused_tags = null; + + self.master_count = 1; + + self.master_factor = 0.6; + + // LeftMaster is the default layout for all outputs + self.layout = Layout.LeftMaster; + + // Set up listeners + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&wlr_output.events.destroy, &self.listen_destroy); + + self.listen_frame.notify = handleFrame; + c.wl_signal_add(&wlr_output.events.frame, &self.listen_frame); + + self.listen_mode.notify = handleMode; + c.wl_signal_add(&wlr_output.events.mode, &self.listen_mode); + + if (c.river_wlr_output_is_noop(wlr_output)) { + // A noop output is always 0 x 0 + self.usable_box = .{ + .x = 0, + .y = 0, + .width = 0, + .height = 0, + }; + } else { + // Add the new output to the layout. The add_auto function arranges outputs + // from left-to-right in the order they appear. A more sophisticated + // compositor would let the user configure the arrangement of outputs in the + // layout. This automatically creates an output global on the wl_display. + c.wlr_output_layout_add_auto(root.wlr_output_layout, wlr_output); + + var width: c_int = undefined; + var height: c_int = undefined; + c.wlr_output_effective_resolution(wlr_output, &width, &height); + self.usable_box = .{ + .x = 0, + .y = 0, + .width = @intCast(u32, width), + .height = @intCast(u32, height), + }; + } +} + +pub fn deinit(self: *Self) void { + for (self.layers) |*layer| { + while (layer.pop()) |layer_surface_node| { + self.root.server.allocator.destroy(layer_surface_node); + } + } + + while (self.views.first) |node| { + node.view.deinit(); + self.views.remove(node); + self.root.server.allocator.destroy(node); + } +} + +pub fn getRenderer(self: Self) *c.wlr_renderer { + return c.river_wlr_backend_get_renderer(self.wlr_output.backend); +} + +const MasterPosition = enum { + Top, + Right, + Bottom, + Left, +}; + +/// Default layout of master-stack and slave-stack. +pub fn layoutMasterStack(self: *Self, visible_count: u32, output_tags: u32, position: MasterPosition) void { + const master_count = std.math.min(self.master_count, visible_count); + const slave_count = if (master_count >= visible_count) 0 else visible_count - master_count; + + const border_width = self.root.server.config.border_width; + const view_padding = self.root.server.config.view_padding; + const outer_padding = self.root.server.config.outer_padding; + + const layout_width = @intCast(u32, self.usable_box.width) - outer_padding * 2; + const layout_height = @intCast(u32, self.usable_box.height) - outer_padding * 2; + + // Depending on position of the master area, + // the *_stack_size is either width or height + var master_stack_size: u32 = undefined; + var slave_stack_size: u32 = undefined; + if (master_count > 0 and slave_count > 0) { + // If both master and slave views are present + if (position == MasterPosition.Right or position == MasterPosition.Left) { + master_stack_size = @floatToInt(u32, @round(@intToFloat(f64, layout_width) * self.master_factor)); + slave_stack_size = layout_width - master_stack_size; + } else { + master_stack_size = @floatToInt(u32, @round(@intToFloat(f64, layout_height) * self.master_factor)); + slave_stack_size = layout_height - master_stack_size; + } + } else if (master_count > 0) { + if (position == MasterPosition.Right or position == MasterPosition.Left) { + master_stack_size = layout_width; + } else { + master_stack_size = layout_height; + } + slave_stack_size = 0; + } else { + if (position == MasterPosition.Right or position == MasterPosition.Left) { + slave_stack_size = layout_width; + } else { + slave_stack_size = layout_height; + } + master_stack_size = 0; + } + + var i: u32 = 0; + var it = ViewStack(View).pendingIterator(self.views.first, output_tags); + while (it.next()) |node| { + const view = &node.view; + + if (view.floating) { + continue; + } + + var new_box: Box = undefined; + + // Add the remainder to the first master/slave to ensure every + // pixel of height is used + if (i < master_count) { + if (position == MasterPosition.Top) { // Top master + const master_width = @divTrunc(layout_width, master_count); + const master_width_rem = layout_width % master_count; + new_box = .{ + .x = @intCast(i32, i * master_width + if (i > 0) master_width_rem else 0), + .y = 0, + .width = master_width + if (i == 0) master_width_rem else 0, + .height = master_stack_size, + }; + } else if (position == MasterPosition.Right) { // Right master + const master_height = @divTrunc(layout_height, master_count); + const master_height_rem = layout_height % master_count; + new_box = .{ + .x = @intCast(i32, slave_stack_size), + .y = @intCast(i32, i * master_height + if (i > 0) master_height_rem else 0), + .width = master_stack_size, + .height = master_height + if (i == 0) master_height_rem else 0, + }; + } else if (position == MasterPosition.Bottom) { // Bottom master + const master_width = @divTrunc(layout_width, master_count); + const master_width_rem = layout_width % master_count; + new_box = .{ + .x = @intCast(i32, i * master_width + if (i > 0) master_width_rem else 0), + .y = @intCast(i32, slave_stack_size), + .width = master_width + if (i == 0) master_width_rem else 0, + .height = master_stack_size, + }; + } else { // Left master + const master_height = @divTrunc(layout_height, master_count); + const master_height_rem = layout_height % master_count; + new_box = .{ + .x = 0, + .y = @intCast(i32, i * master_height + if (i > 0) master_height_rem else 0), + .width = master_stack_size, + .height = master_height + if (i == 0) master_height_rem else 0, + }; + } + } else { + if (position == MasterPosition.Top) { // Top master + const slave_width = @divTrunc(layout_width, slave_count); + const slave_width_rem = layout_width % slave_count; + new_box = .{ + .x = @intCast(i32, (i - master_count) * slave_width + if (i > master_count) slave_width_rem else 0), + .y = @intCast(i32, master_stack_size), + .width = slave_width + if (i == master_count) slave_width_rem else 0, + .height = slave_stack_size, + }; + } else if (position == MasterPosition.Right) { // Right master + const slave_height = @divTrunc(layout_height, slave_count); + const slave_height_rem = layout_height % slave_count; + new_box = .{ + .x = 0, + .y = @intCast(i32, (i - master_count) * slave_height + if (i > master_count) slave_height_rem else 0), + .width = slave_stack_size, + .height = slave_height + if (i == master_count) slave_height_rem else 0, + }; + } else if (position == MasterPosition.Bottom) { // Bottom master + const slave_width = @divTrunc(layout_width, slave_count); + const slave_width_rem = layout_width % slave_count; + new_box = .{ + .x = @intCast(i32, (i - master_count) * slave_width + if (i > master_count) slave_width_rem else 0), + .y = 0, + .width = slave_width + if (i == master_count) slave_width_rem else 0, + .height = slave_stack_size, + }; + } else { // Left master + const slave_height = @divTrunc(layout_height, slave_count); + const slave_height_rem = layout_height % slave_count; + new_box = .{ + .x = @intCast(i32, master_stack_size), + .y = @intCast(i32, (i - master_count) * slave_height + if (i > master_count) slave_height_rem else 0), + .width = slave_stack_size, + .height = slave_height + if (i == master_count) slave_height_rem else 0, + }; + } + } + + // Apply offsets from borders and padding + const xy_offset = @intCast(i32, border_width + outer_padding + view_padding); + new_box.x += self.usable_box.x + xy_offset; + new_box.y += self.usable_box.y + xy_offset; + + // Reduce size to allow space for borders/padding + const delta_size = (border_width + view_padding) * 2; + new_box.width -= delta_size; + new_box.height -= delta_size; + + // Set the view's pending box to the new dimensions + view.pending_box = new_box; + + i += 1; + } +} + +/// Wrapper for default layout with master area on the top +pub fn layoutTopMaster(self: *Self, visible_count: u32, output_tags: u32) void { + layoutMasterStack(self, visible_count, output_tags, MasterPosition.Top); +} + +/// Wrapper for default layout with master area on the right +pub fn layoutRightMaster(self: *Self, visible_count: u32, output_tags: u32) void { + layoutMasterStack(self, visible_count, output_tags, MasterPosition.Right); +} + +/// Wrapper for default layout with master area on the bottom +pub fn layoutBottomMaster(self: *Self, visible_count: u32, output_tags: u32) void { + layoutMasterStack(self, visible_count, output_tags, MasterPosition.Bottom); +} + +/// Wrapper for default layout with master area on the left +pub fn layoutLeftMaster(self: *Self, visible_count: u32, output_tags: u32) void { + layoutMasterStack(self, visible_count, output_tags, MasterPosition.Left); +} + +/// A layout in which every window uses the maximum available space. +pub fn layoutFull(self: *Self, visible_count: u32, output_tags: u32) void { + const border_width = self.root.server.config.border_width; + const view_padding = self.root.server.config.view_padding; + const outer_padding = self.root.server.config.outer_padding; + + const layout_width = @intCast(u32, self.usable_box.width) - + (outer_padding * 2) - (border_width * 2) - (view_padding * 2); + const layout_height = @intCast(u32, self.usable_box.height) - + (outer_padding * 2) - (border_width * 2) - (view_padding * 2); + const x_offset = self.usable_box.x + @intCast(i32, outer_padding + border_width + view_padding); + const y_offset = self.usable_box.y + @intCast(i32, outer_padding + border_width + view_padding); + + var i: u32 = 0; + var it = ViewStack(View).pendingIterator(self.views.first, output_tags); + while (it.next()) |node| { + const view = &node.view; + + if (view.floating) { + continue; + } + + var new_box: Box = undefined; + new_box = .{ + .x = x_offset, + .y = y_offset, + .width = layout_width, + .height = layout_height, + }; + + view.pending_box = new_box; + + i += 1; + } +} + +/// Arrange all views on the output for the current layout. Modifies only +/// pending state, the changes are not appplied until a transaction is started +/// and completed. +pub fn arrangeViews(self: *Self) void { + // If the output has a zero dimension, trying to arrange would cause + // underflow and is pointless anyway + if (self.usable_box.width == 0 or self.usable_box.height == 0) { + return; + } + + const output_tags = if (self.pending_focused_tags) |tags| + tags + else + self.current_focused_tags; + + const visible_count = blk: { + var count: u32 = 0; + var it = ViewStack(View).pendingIterator(self.views.first, output_tags); + while (it.next()) |node| { + if (node.view.floating) { + continue; + } + count += 1; + } + break :blk count; + }; + + // A single view should always use the maximum available space. This is + // implemented via the "full" layout to remove the need of every single + // layout to explicitly handle this edge case or the other edge case of + // no visible views. + if (visible_count <= 1) { + layoutFull(self, visible_count, output_tags); + return; + } + + switch (self.layout) { + .Full => layoutFull(self, visible_count, output_tags), + .TopMaster => layoutTopMaster(self, visible_count, output_tags), + .RightMaster => layoutRightMaster(self, visible_count, output_tags), + .BottomMaster => layoutBottomMaster(self, visible_count, output_tags), + .LeftMaster => layoutLeftMaster(self, visible_count, output_tags), + } +} + +/// Arrange all layer surfaces of this output and addjust the usable aread +pub fn arrangeLayers(self: *Self) void { + const full_box = blk: { + var width: c_int = undefined; + var height: c_int = undefined; + c.wlr_output_effective_resolution(self.wlr_output, &width, &height); + break :blk Box{ + .x = 0, + .y = 0, + .width = @intCast(u32, width), + .height = @intCast(u32, height), + }; + }; + + // This box is modified as exclusive zones are applied + var usable_box = full_box; + + const layer_idxs = [_]usize{ + c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + c.ZWLR_LAYER_SHELL_V1_LAYER_TOP, + c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, + c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, + }; + + // Arrange all layer surfaces with exclusive zones, applying them to the + // usable box along the way. + for (layer_idxs) |layer| { + self.arrangeLayer(self.layers[layer], full_box, &usable_box, true); + } + + // If the the usable_box has changed, we need to rearrange the output + if (!std.meta.eql(self.usable_box, usable_box)) { + self.usable_box = usable_box; + self.root.arrange(); + } + + // Arrange the layers without exclusive zones + for (layer_idxs) |layer| { + self.arrangeLayer(self.layers[layer], full_box, &usable_box, false); + } + + // Find the topmost layer surface in the top or overlay layers which + // requests keyboard interactivity if any. + const topmost_surface = outer: for (layer_idxs[0..2]) |layer| { + // Iterate in reverse order since the last layer is rendered on top + var it = self.layers[layer].last; + while (it) |node| : (it = node.prev) { + const layer_surface = &node.data; + if (layer_surface.wlr_layer_surface.current.keyboard_interactive) { + break :outer layer_surface; + } + } + } else null; + + var it = self.root.server.input_manager.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + + // Only grab focus of seats which have the output focused + if (seat.focused_output != self) { + continue; + } + + if (topmost_surface) |to_focus| { + // If we found a surface that requires focus, grab the focus of all + // seats. + seat.setFocusRaw(.{ .layer = to_focus }); + } else if (seat.focused_layer) |current_focus| { + // If the seat is currently focusing a layer without keyboard + // interactivity, clear the focused layer. + if (!current_focus.wlr_layer_surface.current.keyboard_interactive) { + seat.setFocusRaw(.{ .none = {} }); + seat.focus(null); + } + } + } +} + +/// Arrange the layer surfaces of a given layer +fn arrangeLayer( + self: *Self, + layer: std.TailQueue(LayerSurface), + full_box: Box, + usable_box: *Box, + exclusive: bool, +) void { + var it = layer.first; + while (it) |node| : (it = node.next) { + const layer_surface = &node.data; + const current_state = layer_surface.wlr_layer_surface.current; + + // If the value of exclusive_zone is greater than zero, then it exclusivly + // occupies some area of the screen. + if (exclusive != (current_state.exclusive_zone > 0)) { + continue; + } + + // If the exclusive zone is set to -1, this means the the client would like + // to ignore any exclusive zones and use the full area of the output. + const bounds = if (current_state.exclusive_zone == -1) &full_box else usable_box; + + var new_box: Box = undefined; + + // Horizontal alignment + const anchor_left = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + const anchor_right = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + if (current_state.desired_width == 0) { + const anchor_left_right = anchor_left | anchor_right; + if (current_state.anchor & anchor_left_right == anchor_left_right) { + new_box.x = bounds.x + @intCast(i32, current_state.margin.left); + new_box.width = bounds.width - + (current_state.margin.left + current_state.margin.right); + } else { + Log.Error.log( + "Protocol Error: layer surface '{}' requested width 0 without anchoring to opposite edges.", + .{layer_surface.wlr_layer_surface.namespace}, + ); + c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); + continue; + } + } else if (current_state.anchor & anchor_left != 0) { + new_box.x = bounds.x + @intCast(i32, current_state.margin.left); + new_box.width = current_state.desired_width; + } else if (current_state.anchor & anchor_right != 0) { + new_box.x = bounds.x + @intCast(i32, bounds.width - current_state.desired_width - + current_state.margin.right); + new_box.width = current_state.desired_width; + } else { + new_box.x = bounds.x + @intCast(i32, bounds.width / 2 - current_state.desired_width / 2); + new_box.width = current_state.desired_width; + } + + // Vertical alignment + const anchor_top = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP); + const anchor_bottom = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM); + if (current_state.desired_height == 0) { + const anchor_top_bottom = anchor_top | anchor_bottom; + if (current_state.anchor & anchor_top_bottom == anchor_top_bottom) { + new_box.y = bounds.y + @intCast(i32, current_state.margin.top); + new_box.height = bounds.height - + (current_state.margin.top + current_state.margin.bottom); + } else { + Log.Error.log( + "Protocol Error: layer surface '{}' requested height 0 without anchoring to opposite edges.", + .{layer_surface.wlr_layer_surface.namespace}, + ); + c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); + continue; + } + } else if (current_state.anchor & anchor_top != 0) { + new_box.y = bounds.y + @intCast(i32, current_state.margin.top); + new_box.height = current_state.desired_height; + } else if (current_state.anchor & anchor_bottom != 0) { + new_box.y = bounds.y + @intCast(i32, bounds.height - current_state.desired_height - + current_state.margin.bottom); + new_box.height = current_state.desired_height; + } else { + new_box.y = bounds.y + @intCast(i32, bounds.height / 2 - current_state.desired_height / 2); + new_box.height = current_state.desired_height; + } + + layer_surface.box = new_box; + + // Apply the exclusive zone to the current bounds + const edges = [4]struct { + anchors: u32, + to_increase: ?*i32, + to_decrease: ?*u32, + margin: u32, + }{ + .{ + .anchors = anchor_left | anchor_right | anchor_top, + .to_increase = &usable_box.y, + .to_decrease = &usable_box.height, + .margin = current_state.margin.top, + }, + .{ + .anchors = anchor_left | anchor_right | anchor_bottom, + .to_increase = null, + .to_decrease = &usable_box.height, + .margin = current_state.margin.bottom, + }, + .{ + .anchors = anchor_left | anchor_top | anchor_bottom, + .to_increase = &usable_box.x, + .to_decrease = &usable_box.width, + .margin = current_state.margin.left, + }, + .{ + .anchors = anchor_right | anchor_top | anchor_bottom, + .to_increase = null, + .to_decrease = &usable_box.width, + .margin = current_state.margin.right, + }, + }; + + for (edges) |edge| { + if (current_state.anchor & edge.anchors == edge.anchors and + current_state.exclusive_zone + @intCast(i32, edge.margin) > 0) + { + const delta = current_state.exclusive_zone + @intCast(i32, edge.margin); + if (edge.to_increase) |value| { + value.* += delta; + } + if (edge.to_decrease) |value| { + value.* -= @intCast(u32, delta); + } + } + } + + // Tell the client to assume the new size + Log.Debug.log("send configure, {} x {}", .{ layer_surface.box.width, layer_surface.box.height }); + c.wlr_layer_surface_v1_configure( + layer_surface.wlr_layer_surface, + layer_surface.box.width, + layer_surface.box.height, + ); + } +} + +/// Called when the output is destroyed. Evacuate all views from the output +/// and then remove it from the list of outputs. +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + const root = self.root; + + Log.Debug.log("Output {} destroyed", .{self.wlr_output.name}); + + // Use the first output in the list that is not the one being destroyed. + // If there is no other real output, use the noop output. + var output_it = root.outputs.first; + const fallback_output = while (output_it) |output_node| : (output_it = output_node.next) { + if (&output_node.data != self) { + break &output_node.data; + } + } else &root.noop_output; + + // Move all views from the destroyed output to the fallback one + while (self.views.last) |node| { + const view = &node.view; + view.sendToOutput(fallback_output); + } + + // Close all layer surfaces on the destroyed output + for (self.layers) |*layer, layer_idx| { + while (layer.pop()) |node| { + const layer_surface = &node.data; + // We need to move the closing layer surface to the noop output + // since it may not be immediately destoryed. This just a request + // to close which will trigger unmap and destroy events in + // response, and the LayerSurface needs a valid output to + // handle them. + root.noop_output.layers[layer_idx].prepend(node); + layer_surface.output = &root.noop_output; + c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); + } + } + + // If any seat has the destroyed output focused, focus the fallback one + var seat_it = root.server.input_manager.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + const seat = &seat_node.data; + if (seat.focused_output == self) { + seat.focused_output = fallback_output; + seat.focus(null); + } + } + + // Remove all listeners + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_frame.link); + c.wl_list_remove(&self.listen_mode.link); + + // Clean up the wlr_output + self.wlr_output.data = null; + + // Remove the destroyed output from the list + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + root.outputs.remove(node); + root.server.allocator.destroy(node); + + // Arrange the root in case evacuated views affect the layout + root.arrange(); +} + +fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This function is called every time an output is ready to display a frame, + // generally at the output's refresh rate (e.g. 60Hz). + const self = @fieldParentPtr(Self, "listen_frame", listener.?); + render.renderOutput(self); +} + +fn handleMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_mode", listener.?); + self.arrangeLayers(); + self.root.arrange(); +} diff --git a/river/Root.zig b/river/Root.zig new file mode 100644 index 0000000..f87d7ae --- /dev/null +++ b/river/Root.zig @@ -0,0 +1,247 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); +const build_options = @import("build_options"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const Server = @import("Server.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); + +/// Responsible for all windowing operations +server: *Server, + +wlr_output_layout: *c.wlr_output_layout, +outputs: std.TailQueue(Output), + +/// This output is used internally when no real outputs are available. +/// It is not advertised to clients. +noop_output: Output, + +/// This list stores all unmanaged Xwayland windows. This needs to be in root +/// since X is like the wild west and who knows where these things will go. +xwayland_unmanaged_views: if (build_options.xwayland) std.TailQueue(XwaylandUnmanaged) else void, + +/// Number of pending configures sent in the current transaction. +/// A value of 0 means there is no current transaction. +pending_configures: u32, + +/// Handles timeout of transactions +transaction_timer: ?*c.wl_event_source, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + + // Create an output layout, which a wlroots utility for working with an + // arrangement of screens in a physical layout. + self.wlr_output_layout = c.wlr_output_layout_create() orelse + return error.CantCreateWlrOutputLayout; + errdefer c.wlr_output_layout_destroy(self.wlr_output_layout); + + self.outputs = std.TailQueue(Output).init(); + + const noop_wlr_output = c.river_wlr_noop_add_output(server.noop_backend) orelse + return error.CantAddNoopOutput; + try self.noop_output.init(self, noop_wlr_output); + + if (build_options.xwayland) { + self.xwayland_unmanaged_views = std.TailQueue(XwaylandUnmanaged).init(); + } + + self.pending_configures = 0; + + self.transaction_timer = null; +} + +pub fn deinit(self: *Self) void { + while (self.outputs.pop()) |output_node| { + output_node.data.deinit(); + self.server.allocator.destroy(output_node); + } + c.wlr_output_layout_destroy(self.wlr_output_layout); +} + +pub fn addOutput(self: *Self, wlr_output: *c.wlr_output) void { + // TODO: Handle failure + const node = self.outputs.allocateNode(self.server.allocator) catch unreachable; + node.data.init(self, wlr_output) catch unreachable; + self.outputs.append(node); + + // if we previously had no real outputs, move focus from the noop output + // to the new one. + if (self.outputs.len == 1) { + // TODO: move views from the noop output to the new one and focus(null) + var it = self.server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + seat_node.data.focused_output = &self.outputs.first.?.data; + } + } +} + +/// Clear the current focus. +pub fn clearFocus(self: *Self) void { + if (self.focused_view) |view| { + _ = c.wlr_xdg_toplevel_set_activated(view.wlr_xdg_surface, false); + } + self.focused_view = null; +} + +/// Arrange all views on all outputs and then start a transaction. +pub fn arrange(self: *Self) void { + var it = self.outputs.first; + while (it) |output_node| : (it = output_node.next) { + output_node.data.arrangeViews(); + } + self.startTransaction(); +} + +/// Initiate an atomic change to the layout. This change will not be +/// applied until all affected clients ack a configure and commit a buffer. +fn startTransaction(self: *Self) void { + // If a new transaction is started while another is in progress, we need + // to reset the pending count to 0 and clear serials from the views + self.pending_configures = 0; + + // Iterate over all views of all outputs + var output_it = self.outputs.first; + while (output_it) |node| : (output_it = node.next) { + const output = &node.data; + var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); + while (view_it.next()) |view_node| { + const view = &view_node.view; + // Clear the serial in case this transaction is interrupting a prior one. + view.pending_serial = null; + + if (view.needsConfigure()) { + view.configure(); + self.pending_configures += 1; + + // We save the current buffer, so we can send an early + // frame done event to give the client a head start on + // redrawing. + view.sendFrameDone(); + } + + // If there is a saved buffer present, then this transaction is interrupting + // a previous transaction and we should keep the old buffer. + if (view.stashed_buffer == null) { + view.stashBuffer(); + } + } + } + + if (self.pending_configures > 0) { + Log.Debug.log( + "Started transaction with {} pending configures.", + .{self.pending_configures}, + ); + + // TODO: log failure to create timer and commit immediately + self.transaction_timer = c.wl_event_loop_add_timer( + self.server.wl_event_loop, + handleTimeout, + self, + ); + + // Set timeout to 200ms + if (c.wl_event_source_timer_update(self.transaction_timer, 200) == -1) { + // TODO: handle failure + } + } else { + self.commitTransaction(); + } +} + +fn handleTimeout(data: ?*c_void) callconv(.C) c_int { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); + + Log.Error.log("Transaction timed out. Some imperfect frames may be shown.", .{}); + + self.commitTransaction(); + + return 0; +} + +pub fn notifyConfigured(self: *Self) void { + self.pending_configures -= 1; + if (self.pending_configures == 0) { + // Stop the timer, as we didn't timeout + if (c.wl_event_source_timer_update(self.transaction_timer, 0) == -1) { + // TODO: handle failure + } + self.commitTransaction(); + } +} + +/// Apply the pending state and drop stashed buffers. This means that +/// the next frame drawn will be the post-transaction state of the +/// layout. Should only be called after all clients have configured for +/// the new layout. If called early imperfect frames may be drawn. +fn commitTransaction(self: *Self) void { + // TODO: apply damage properly + + // Ensure this is set to 0 to avoid entering invalid state (e.g. if called due to timeout) + self.pending_configures = 0; + + // Iterate over all views of all outputs + var output_it = self.outputs.first; + while (output_it) |output_node| : (output_it = output_node.next) { + const output = &output_node.data; + + // If there were pending focused tags, make them the current focus + if (output.pending_focused_tags) |tags| { + Log.Debug.log( + "changing current focus: {b:0>10} to {b:0>10}", + .{ output.current_focused_tags, tags }, + ); + output.current_focused_tags = tags; + output.pending_focused_tags = null; + } + + var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); + while (view_it.next()) |view_node| { + const view = &view_node.view; + // Ensure that all pending state is cleared + view.pending_serial = null; + if (view.pending_box) |state| { + view.current_box = state; + view.pending_box = null; + } + + // Apply possible pending tags + if (view.pending_tags) |tags| { + view.current_tags = tags; + view.pending_tags = null; + } + + view.dropStashedBuffer(); + } + } + + // Iterate over all seats and update focus + var it = self.server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + seat_node.data.focus(null); + } +} diff --git a/river/Seat.zig b/river/Seat.zig new file mode 100644 index 0000000..82979fc --- /dev/null +++ b/river/Seat.zig @@ -0,0 +1,312 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); +const command = @import("command.zig"); + +const Cursor = @import("Cursor.zig"); +const InputManager = @import("InputManager.zig"); +const Keyboard = @import("Keyboard.zig"); +const LayerSurface = @import("LayerSurface.zig"); +const Output = @import("Output.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +const FocusTarget = union(enum) { + view: *View, + layer: *LayerSurface, + none: void, +}; + +input_manager: *InputManager, +wlr_seat: *c.wlr_seat, + +/// Multiple mice are handled by the same Cursor +cursor: Cursor, + +/// Mulitple keyboards are handled separately +keyboards: std.TailQueue(Keyboard), + +/// ID of the current keymap mode +mode_id: usize, + +/// Currently focused output, may be the noop output if no +focused_output: *Output, + +/// Currently focused view if any +focused_view: ?*View, + +/// Stack of views in most recently focused order +/// If there is a currently focused view, it is on top. +focus_stack: ViewStack(*View), + +/// Currently focused layer, if any. While this is non-null, no views may +/// recieve focus. +focused_layer: ?*LayerSurface, + +listen_request_set_selection: c.wl_listener, + +pub fn init(self: *Self, input_manager: *InputManager, name: []const u8) !void { + self.input_manager = input_manager; + + // This will be automatically destroyed when the display is destroyed + self.wlr_seat = c.wlr_seat_create(input_manager.server.wl_display, name.ptr) orelse + return error.CantCreateWlrSeat; + + try self.cursor.init(self); + errdefer self.cursor.destroy(); + + self.keyboards = std.TailQueue(Keyboard).init(); + + self.mode_id = 0; + + self.focused_output = &self.input_manager.server.root.noop_output; + + self.focused_view = null; + + self.focus_stack.init(); + + self.focused_layer = null; + + self.listen_request_set_selection.notify = handleRequestSetSelection; + c.wl_signal_add(&self.wlr_seat.events.request_set_selection, &self.listen_request_set_selection); +} + +pub fn deinit(self: *Self) void { + self.cursor.deinit(); + + while (self.keyboards.pop()) |node| { + self.input_manager.server.allocator.destroy(node); + } + + while (self.focus_stack.first) |node| { + self.focus_stack.remove(node); + self.input_manager.server.allocator.destroy(node); + } +} + +/// Set the current focus. If a visible view is passed it will be focused. +/// If null is passed, the first visible view in the focus stack will be focused. +pub fn focus(self: *Self, _view: ?*View) void { + var view = _view; + + // While a layer surface is focused, views may not recieve focus + if (self.focused_layer != null) { + std.debug.assert(self.focused_view == null); + return; + } + + // If view is null or not currently visible + if (if (view) |v| + v.output != self.focused_output or + v.current_tags & self.focused_output.current_focused_tags == 0 + else + true) { + // Set view to the first currently visible view on in the focus stack if any + var it = ViewStack(*View).iterator( + self.focus_stack.first, + self.focused_output.current_focused_tags, + ); + view = while (it.next()) |node| { + if (node.view.output == self.focused_output) { + break node.view; + } + } else null; + } + + if (view) |view_to_focus| { + // Find or allocate a new node in the focus stack for the target view + var it = self.focus_stack.first; + while (it) |node| : (it = node.next) { + // If the view is found, move it to the top of the stack + if (node.view == view_to_focus) { + const new_focus_node = self.focus_stack.remove(node); + self.focus_stack.push(node); + break; + } + } else { + // The view is not in the stack, so allocate a new node and prepend it + const new_focus_node = self.input_manager.server.allocator.create( + ViewStack(*View).Node, + ) catch unreachable; + new_focus_node.view = view_to_focus; + self.focus_stack.push(new_focus_node); + } + + // Focus the target view + self.setFocusRaw(.{ .view = view_to_focus }); + } else { + // Otherwise clear the focus + self.setFocusRaw(.{ .none = {} }); + } +} + +/// Switch focus to the target, handling unfocus and input inhibition +/// properly. This should only be called directly if dealing with layers. +pub fn setFocusRaw(self: *Self, focus_target: FocusTarget) void { + // If the target is already focused, do nothing + if (switch (focus_target) { + .view => |target_view| target_view == self.focused_view, + .layer => |target_layer| target_layer == self.focused_layer, + .none => false, + }) { + return; + } + + // Obtain the target wlr_surface + const target_wlr_surface = switch (focus_target) { + .view => |target_view| target_view.wlr_surface.?, + .layer => |target_layer| target_layer.wlr_layer_surface.surface.?, + .none => null, + }; + + // If input is not allowed on the target surface (e.g. due to an active + // input inhibitor) do not set focus. If there is no target surface we + // still clear the focus. + if (if (target_wlr_surface) |wlr_surface| + self.input_manager.inputAllowed(wlr_surface) + else + true) { + // First clear the current focus + if (self.focused_view) |current_focus| { + std.debug.assert(self.focused_layer == null); + current_focus.setFocused(false); + self.focused_view = null; + } + if (self.focused_layer) |current_focus| { + std.debug.assert(self.focused_view == null); + self.focused_layer = null; + } + c.wlr_seat_keyboard_clear_focus(self.wlr_seat); + + // Set the new focus + switch (focus_target) { + .view => |target_view| { + std.debug.assert(self.focused_output == target_view.output); + target_view.setFocused(true); + self.focused_view = target_view; + }, + .layer => |target_layer| blk: { + std.debug.assert(self.focused_output == target_layer.output); + self.focused_layer = target_layer; + }, + .none => {}, + } + + // Tell wlroots to send the new keyboard focus if we have a target + if (target_wlr_surface) |wlr_surface| { + const keyboard: *c.wlr_keyboard = c.wlr_seat_get_keyboard(self.wlr_seat); + c.wlr_seat_keyboard_notify_enter( + self.wlr_seat, + wlr_surface, + &keyboard.keycodes, + keyboard.num_keycodes, + &keyboard.modifiers, + ); + } + } +} + +/// Handle the unmapping of a view, removing it from the focus stack and +/// setting the focus if needed. +pub fn handleViewUnmap(self: *Self, view: *View) void { + // Remove the node from the focus stack and destroy it. + var it = self.focus_stack.first; + while (it) |node| : (it = node.next) { + if (node.view == view) { + self.focus_stack.remove(node); + self.input_manager.server.allocator.destroy(node); + break; + } + } + + // If the unmapped view is focused, choose a new focus + if (self.focused_view) |current_focus| { + if (current_focus == view) { + self.focus(null); + } + } +} + +/// Handle any user-defined mapping for the passed keysym and modifiers +/// Returns true if the key was handled +pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool { + const modes = &self.input_manager.server.config.modes; + for (modes.items[self.mode_id].items) |mapping| { + if (modifiers == mapping.modifiers and keysym == mapping.keysym) { + // Execute the bound command + const allocator = self.input_manager.server.allocator; + var failure_message: []const u8 = undefined; + command.run(allocator, self, mapping.command_args, &failure_message) catch |err| { + // TODO: log the error + if (err == command.Error.CommandFailed) + allocator.free(failure_message); + }; + return true; + } + } + return false; +} + +/// Add a newly created input device to the seat and update the reported +/// capabilities. +pub fn addDevice(self: *Self, device: *c.wlr_input_device) !void { + switch (device.type) { + .WLR_INPUT_DEVICE_KEYBOARD => self.addKeyboard(device) catch unreachable, + .WLR_INPUT_DEVICE_POINTER => self.addPointer(device), + else => {}, + } + + // We need to let the wlr_seat know what our capabilities are, which is + // communiciated to the client. We always have a cursor, even if + // there are no pointer devices, so we always include that capability. + var caps = @intCast(u32, c.WL_SEAT_CAPABILITY_POINTER); + // if list not empty + if (self.keyboards.len > 0) { + caps |= @intCast(u32, c.WL_SEAT_CAPABILITY_KEYBOARD); + } + c.wlr_seat_set_capabilities(self.wlr_seat, caps); +} + +fn addKeyboard(self: *Self, device: *c.wlr_input_device) !void { + c.wlr_seat_set_keyboard(self.wlr_seat, device); + + const node = try self.keyboards.allocateNode(self.input_manager.server.allocator); + try node.data.init(self, device); + self.keyboards.append(node); +} + +fn addPointer(self: Self, device: *c.struct_wlr_input_device) void { + // We don't do anything special with pointers. All of our pointer handling + // is proxied through wlr_cursor. On another compositor, you might take this + // opportunity to do libinput configuration on the device to set + // acceleration, etc. + c.wlr_cursor_attach_input_device(self.cursor.wlr_cursor, device); +} + +fn handleRequestSetSelection(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_request_set_selection", listener.?); + const event = @ptrCast( + *c.wlr_seat_request_set_selection_event, + @alignCast(@alignOf(*c.wlr_seat_request_set_selection_event), data), + ); + c.wlr_seat_set_selection(self.wlr_seat, event.source, event.serial); +} diff --git a/river/Server.zig b/river/Server.zig new file mode 100644 index 0000000..e46a619 --- /dev/null +++ b/river/Server.zig @@ -0,0 +1,279 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const build_options = @import("build_options"); +const std = @import("std"); + +const c = @import("c.zig"); + +const Config = @import("Config.zig"); +const DecorationManager = @import("DecorationManager.zig"); +const InputManager = @import("InputManager.zig"); +const LayerSurface = @import("LayerSurface.zig"); +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const Root = @import("Root.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const Control = @import("Control.zig"); +const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); + +allocator: *std.mem.Allocator, + +wl_display: *c.wl_display, +wl_event_loop: *c.wl_event_loop, + +wlr_backend: *c.wlr_backend, +noop_backend: *c.wlr_backend, +listen_new_output: c.wl_listener, + +wlr_xdg_shell: *c.wlr_xdg_shell, +listen_new_xdg_surface: c.wl_listener, + +wlr_layer_shell: *c.wlr_layer_shell_v1, +listen_new_layer_surface: c.wl_listener, + +wlr_xwayland: if (build_options.xwayland) *c.wlr_xwayland else void, +listen_new_xwayland_surface: if (build_options.xwayland) c.wl_listener else void, + +decoration_manager: DecorationManager, +input_manager: InputManager, +root: Root, +config: Config, +control: Control, + +pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { + self.allocator = allocator; + + // The Wayland display is managed by libwayland. It handles accepting + // clients from the Unix socket, managing Wayland globals, and so on. + self.wl_display = c.wl_display_create() orelse + return error.CantCreateWlDisplay; + errdefer c.wl_display_destroy(self.wl_display); + + // Should never return null if the display was created successfully + self.wl_event_loop = c.wl_display_get_event_loop(self.wl_display) orelse + return error.CantGetEventLoop; + + // The wlr_backend abstracts the input/output hardware. Autocreate chooses + // the best option based on the environment, for example DRM when run from + // a tty or wayland if WAYLAND_DISPLAY is set. This frees itself when the + // wl_display is destroyed. + self.wlr_backend = c.river_wlr_backend_autocreate(self.wl_display) orelse + return error.CantCreateWlrBackend; + + // This backend is used to create a noop output for use when no actual + // outputs are available. This frees itself when the wl_display is destroyed. + self.noop_backend = c.river_wlr_noop_backend_create(self.wl_display) orelse + return error.CantCreateNoopBackend; + + // If we don't provide a renderer, autocreate makes a GLES2 renderer for us. + // The renderer is responsible for defining the various pixel formats it + // supports for shared memory, this configures that for clients. + const wlr_renderer = c.river_wlr_backend_get_renderer(self.wlr_backend) orelse + return error.CantGetWlrRenderer; + // TODO: Handle failure after https://github.com/swaywm/wlroots/pull/2080 + c.wlr_renderer_init_wl_display(wlr_renderer, self.wl_display); // orelse + // return error.CantInitWlDisplay; + self.listen_new_output.notify = handleNewOutput; + c.wl_signal_add(&self.wlr_backend.events.new_output, &self.listen_new_output); + + const wlr_compositor = c.wlr_compositor_create(self.wl_display, wlr_renderer) orelse + return error.CantCreateWlrCompositor; + + // Set up xdg shell + self.wlr_xdg_shell = c.wlr_xdg_shell_create(self.wl_display) orelse + return error.CantCreateWlrXdgShell; + self.listen_new_xdg_surface.notify = handleNewXdgSurface; + c.wl_signal_add(&self.wlr_xdg_shell.events.new_surface, &self.listen_new_xdg_surface); + + // Set up layer shell + self.wlr_layer_shell = c.wlr_layer_shell_v1_create(self.wl_display) orelse + return error.CantCreateWlrLayerShell; + self.listen_new_layer_surface.notify = handleNewLayerSurface; + c.wl_signal_add(&self.wlr_layer_shell.events.new_surface, &self.listen_new_layer_surface); + + // Set up xwayland if built with support + if (build_options.xwayland) { + self.wlr_xwayland = c.wlr_xwayland_create(self.wl_display, wlr_compositor, false) orelse + return error.CantCreateWlrXwayland; + self.listen_new_xwayland_surface.notify = handleNewXwaylandSurface; + c.wl_signal_add(&self.wlr_xwayland.events.new_surface, &self.listen_new_xwayland_surface); + } + + try self.config.init(self.allocator); + try self.decoration_manager.init(self); + try self.root.init(self); + // Must be called after root is initialized + try self.input_manager.init(self); + try self.control.init(self); + + // These all free themselves when the wl_display is destroyed + _ = c.wlr_data_device_manager_create(self.wl_display) orelse + return error.CantCreateWlrDataDeviceManager; + _ = c.wlr_screencopy_manager_v1_create(self.wl_display) orelse + return error.CantCreateWlrScreencopyManager; + _ = c.wlr_xdg_output_manager_v1_create(self.wl_display, self.root.wlr_output_layout) orelse + return error.CantCreateWlrOutputManager; +} + +/// Free allocated memory and clean up +pub fn deinit(self: *Self) void { + // Note: order is important here + if (build_options.xwayland) { + c.wlr_xwayland_destroy(self.wlr_xwayland); + } + c.wl_display_destroy_clients(self.wl_display); + c.wl_display_destroy(self.wl_display); + self.input_manager.deinit(); + self.root.deinit(); + self.config.deinit(self.allocator); +} + +/// Create the socket, set WAYLAND_DISPLAY, and start the backend +pub fn start(self: Self) !void { + // Add a Unix socket to the Wayland display. + const socket = c.wl_display_add_socket_auto(self.wl_display) orelse + return error.CantAddSocket; + + // Start the backend. This will enumerate outputs and inputs, become the DRM + // master, etc + if (!c.river_wlr_backend_start(self.wlr_backend)) { + return error.CantStartBackend; + } + + // Set the WAYLAND_DISPLAY environment variable to our socket + if (c.setenv("WAYLAND_DISPLAY", socket, 1) == -1) { + return error.CantSetEnv; + } + + if (build_options.xwayland) { + if (c.setenv("DISPLAY", &self.wlr_xwayland.display_name, 1) == -1) { + return error.CantSetEnv; + } + } +} + +/// Enter the wayland event loop and block until the compositor is exited +pub fn run(self: Self) void { + c.wl_display_run(self.wl_display); +} + +fn handleNewOutput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_output", listener.?); + const wlr_output = @ptrCast(*c.wlr_output, @alignCast(@alignOf(*c.wlr_output), data)); + Log.Debug.log("New output {}", .{wlr_output.name}); + self.root.addOutput(wlr_output); +} + +fn handleNewXdgSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when wlr_xdg_shell receives a new xdg surface from a + // client, either a toplevel (application window) or popup. + const self = @fieldParentPtr(Self, "listen_new_xdg_surface", listener.?); + const wlr_xdg_surface = @ptrCast(*c.wlr_xdg_surface, @alignCast(@alignOf(*c.wlr_xdg_surface), data)); + + if (wlr_xdg_surface.role == .WLR_XDG_SURFACE_ROLE_POPUP) { + Log.Debug.log("New xdg_popup", .{}); + return; + } + + Log.Debug.log("New xdg_toplevel", .{}); + + // The View will add itself to the output's view stack on map + const output = self.input_manager.default_seat.focused_output; + const node = self.allocator.create(ViewStack(View).Node) catch unreachable; + node.view.init(output, output.current_focused_tags, wlr_xdg_surface); +} + +/// This event is raised when the layer_shell recieves a new surface from a client. +fn handleNewLayerSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_layer_surface", listener.?); + const wlr_layer_surface = @ptrCast( + *c.wlr_layer_surface_v1, + @alignCast(@alignOf(*c.wlr_layer_surface_v1), data), + ); + + Log.Debug.log( + "New layer surface: namespace {}, layer {}, anchor {}, size {}x{}, margin ({},{},{},{}), exclusive_zone {}", + .{ + wlr_layer_surface.namespace, + wlr_layer_surface.client_pending.layer, + wlr_layer_surface.client_pending.anchor, + wlr_layer_surface.client_pending.desired_width, + wlr_layer_surface.client_pending.desired_height, + wlr_layer_surface.client_pending.margin.top, + wlr_layer_surface.client_pending.margin.right, + wlr_layer_surface.client_pending.margin.bottom, + wlr_layer_surface.client_pending.margin.left, + wlr_layer_surface.client_pending.exclusive_zone, + }, + ); + + // If the new layer surface does not have an output assigned to it, use the + // first output or close the surface if none are available. + if (wlr_layer_surface.output == null) { + if (self.root.outputs.first) |node| { + const output = &node.data; + Log.Debug.log( + "New layer surface had null output, assigning it to output {}", + .{output.wlr_output.name}, + ); + wlr_layer_surface.output = output.wlr_output; + } else { + Log.Error.log( + "No output available for layer surface '{}'", + .{wlr_layer_surface.namespace}, + ); + c.wlr_layer_surface_v1_close(wlr_layer_surface); + return; + } + } + + // The layer surface will add itself to the proper list of the output on map + const output = @ptrCast(*Output, @alignCast(@alignOf(*Output), wlr_layer_surface.output.*.data)); + const node = self.allocator.create(std.TailQueue(LayerSurface).Node) catch unreachable; + node.data.init(output, wlr_layer_surface); +} + +fn handleNewXwaylandSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_xwayland_surface", listener.?); + const wlr_xwayland_surface = @ptrCast( + *c.wlr_xwayland_surface, + @alignCast(@alignOf(*c.wlr_xwayland_surface), data), + ); + + if (wlr_xwayland_surface.override_redirect) { + Log.Debug.log("New unmanaged xwayland surface", .{}); + // The unmanged surface will add itself to the list of unmanaged views + // in Root when it is mapped. + const node = self.allocator.create(std.TailQueue(XwaylandUnmanaged).Node) catch unreachable; + node.data.init(&self.root, wlr_xwayland_surface); + return; + } + + Log.Debug.log( + "New xwayland surface: title '{}', class '{}'", + .{ wlr_xwayland_surface.title, wlr_xwayland_surface.class }, + ); + + // The View will add itself to the output's view stack on map + const output = self.input_manager.default_seat.focused_output; + const node = self.allocator.create(ViewStack(View).Node) catch unreachable; + node.view.init(output, output.current_focused_tags, wlr_xwayland_surface); +} diff --git a/river/View.zig b/river/View.zig new file mode 100644 index 0000000..8ab32cc --- /dev/null +++ b/river/View.zig @@ -0,0 +1,271 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const build_options = @import("build_options"); +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const Root = @import("Root.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XdgToplevel = @import("XdgToplevel.zig"); +const XwaylandView = if (build_options.xwayland) + @import("XwaylandView.zig") +else + @import("VoidView.zig"); + +const Impl = union(enum) { + xdg_toplevel: XdgToplevel, + xwayland_view: XwaylandView, +}; + +/// The implementation of this view +impl: Impl, + +/// The output this view is currently associated with +output: *Output, + +/// This is non-null exactly when the view is mapped +wlr_surface: ?*c.wlr_surface, + +/// If the view is floating or not +floating: bool, + +/// True if the view is currentlt focused by at lease one seat +focused: bool, + +/// The current output-relative coordinates and dimensions of the view +current_box: Box, +pending_box: ?Box, + +/// The dimensions the view would have taken if we didn't force it to tile +natural_width: u32, +natural_height: u32, + +current_tags: u32, +pending_tags: ?u32, + +pending_serial: ?u32, + +// This is what we render while a transaction is in progress +stashed_buffer: ?*c.wlr_buffer, + +pub fn init( + self: *Self, + output: *Output, + tags: u32, + surface: var, +) void { + self.output = output; + + self.wlr_surface = null; + + self.focused = false; + + self.current_box = Box{ + .x = 0, + .y = 0, + .height = 0, + .width = 0, + }; + self.pending_box = null; + + self.current_tags = tags; + self.pending_tags = null; + + self.pending_serial = null; + + self.stashed_buffer = null; + + if (@TypeOf(surface) == *c.wlr_xdg_surface) { + self.impl = .{ .xdg_toplevel = undefined }; + self.impl.xdg_toplevel.init(self, surface); + } else if (build_options.xwayland and @TypeOf(surface) == *c.wlr_xwayland_surface) { + self.impl = .{ .xwayland_view = undefined }; + self.impl.xwayland_view.init(self, surface); + } else unreachable; +} + +pub fn deinit(self: Self) void { + if (self.stashed_buffer) |buffer| { + c.wlr_buffer_unref(buffer); + } +} + +pub fn needsConfigure(self: Self) bool { + if (self.pending_box) |pending_box| { + return pending_box.width != self.current_box.width or + pending_box.height != self.current_box.height; + } else { + return false; + } +} + +pub fn configure(self: Self) void { + if (self.pending_box) |pending_box| { + switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.configure(pending_box), + .xwayland_view => |xwayland_view| xwayland_view.configure(pending_box), + } + } else { + Log.Error.log("Configure called on a View with no pending box", .{}); + } +} + +pub fn sendFrameDone(self: Self) void { + var now: c.timespec = undefined; + _ = c.clock_gettime(c.CLOCK_MONOTONIC, &now); + c.wlr_surface_send_frame_done(self.wlr_surface.?, &now); +} + +pub fn dropStashedBuffer(self: *Self) void { + // TODO: log debug error + if (self.stashed_buffer) |buffer| { + c.wlr_buffer_unref(buffer); + self.stashed_buffer = null; + } +} + +pub fn stashBuffer(self: *Self) void { + // TODO: log debug error if there is already a saved buffer + if (self.wlr_surface) |wlr_surface| { + if (c.wlr_surface_has_buffer(wlr_surface)) { + _ = c.wlr_buffer_ref(wlr_surface.buffer); + self.stashed_buffer = wlr_surface.buffer; + } + } +} + +/// Set the focued bool and the active state of the view if it is a toplevel +pub fn setFocused(self: *Self, focused: bool) void { + self.focused = focused; + switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.setActivated(focused), + .xwayland_view => |xwayland_view| xwayland_view.setActivated(focused), + } +} + +/// If true is passsed, make the view float. If false, return it to the tiled +/// layout. +pub fn setFloating(self: *Self, float: bool) void { + if (float and !self.floating) { + self.floating = true; + self.pending_box = Box{ + .x = std.math.max(0, @divTrunc(@intCast(i32, self.output.usable_box.width) - + @intCast(i32, self.natural_width), 2)), + .y = std.math.max(0, @divTrunc(@intCast(i32, self.output.usable_box.height) - + @intCast(i32, self.natural_height), 2)), + .width = self.natural_width, + .height = self.natural_height, + }; + } else if (!float and self.floating) { + self.floating = false; + } +} + +/// Move a view from one output to another, sending the required enter/leave +/// events. +pub fn sendToOutput(self: *Self, destination_output: *Output) void { + const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); + + self.output.views.remove(node); + destination_output.views.push(node); + + c.wlr_surface_send_leave(self.wlr_surface, self.output.wlr_output); + c.wlr_surface_send_enter(self.wlr_surface, destination_output.wlr_output); + + self.output = destination_output; +} + +pub fn close(self: Self) void { + switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.close(), + .xwayland_view => |xwayland_view| xwayland_view.close(), + } +} + +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.forEachSurface(iterator, user_data), + .xwayland_view => |xwayland_view| xwayland_view.forEachSurface(iterator, user_data), + } +} + +/// Return the surface at output coordinates ox, oy and set sx, sy to the +/// corresponding surface-relative coordinates, if there is a surface. +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + return switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.surfaceAt(ox, oy, sx, sy), + .xwayland_view => |xwayland_view| xwayland_view.surfaceAt(ox, oy, sx, sy), + }; +} + +/// Called by the impl when the surface is ready to be displayed +pub fn map(self: *Self) void { + const root = self.output.root; + + // Add the view to the stack of its output + const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); + self.output.views.push(node); + + // Focus the newly mapped view. Note: if a seat is focusing a different output + // it will continue to do so. + var it = root.server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + seat_node.data.focus(self); + } + + c.wlr_surface_send_enter(self.wlr_surface.?, self.output.wlr_output); + + root.arrange(); +} + +/// Called by the impl when the surface will no longer be displayed +pub fn unmap(self: *Self) void { + const root = self.output.root; + + self.wlr_surface = null; + + // Inform all seats that the view has been unmapped so they can handle focus + var it = root.server.input_manager.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + seat.handleViewUnmap(self); + } + + // Remove the view from its output's stack + const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); + self.output.views.remove(node); + + root.arrange(); +} + +/// Destory the view and free the ViewStack node holding it. +pub fn destroy(self: *const Self) void { + self.deinit(); + const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); + self.output.root.server.allocator.destroy(node); +} diff --git a/river/VoidView.zig b/river/VoidView.zig new file mode 100644 index 0000000..72c57a7 --- /dev/null +++ b/river/VoidView.zig @@ -0,0 +1,48 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); + +pub fn configure(self: Self, pending_box: Box) void { + unreachable; +} + +pub fn setActivated(self: Self, activated: bool) void { + unreachable; +} + +pub fn close(self: Self) void { + unreachable; +} + +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + unreachable; +} + +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + unreachable; +} diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig new file mode 100644 index 0000000..b4ee56c --- /dev/null +++ b/river/XdgPopup.zig @@ -0,0 +1,82 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Output = @import("Output.zig"); + +/// The output this popup is displayed on. +output: *Output, + +/// Box of the parent of this popup tree. Needed to unconstrain child popups. +parent_box: *const Box, + +/// The corresponding wlroots object +wlr_xdg_popup: *c.wlr_xdg_popup, + +listen_destroy: c.wl_listener, +listen_new_popup: c.wl_listener, + +pub fn init( + self: *Self, + output: *Output, + parent_box: *const Box, + wlr_xdg_popup: *c.wlr_xdg_popup, +) void { + self.output = output; + self.parent_box = parent_box; + self.wlr_xdg_popup = wlr_xdg_popup; + + // The output box relative to the parent of the popup + var box = c.wlr_output_layout_get_box(output.root.wlr_output_layout, output.wlr_output).*; + box.x -= parent_box.x; + box.y -= parent_box.y; + c.wlr_xdg_popup_unconstrain_from_box(wlr_xdg_popup, &box); + + // Setup listeners + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&wlr_xdg_popup.base.*.events.destroy, &self.listen_destroy); + + self.listen_new_popup.notify = handleNewPopup; + c.wl_signal_add(&wlr_xdg_popup.base.*.events.new_popup, &self.listen_new_popup); +} + +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + const allocator = self.output.root.server.allocator; + + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_new_popup.link); + + allocator.destroy(self); +} + +/// Called when a new xdg popup is requested by the client +fn handleNewPopup(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_popup", listener.?); + const wlr_xdg_popup = @ptrCast(*c.wlr_xdg_popup, @alignCast(@alignOf(*c.wlr_xdg_popup), data)); + const allocator = self.output.root.server.allocator; + + // This will free itself on destroy + var xdg_popup = allocator.create(Self) catch unreachable; + xdg_popup.init(self.output, self.parent_box, wlr_xdg_popup); +} diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig new file mode 100644 index 0000000..b3806db --- /dev/null +++ b/river/XdgToplevel.zig @@ -0,0 +1,201 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XdgPopup = @import("XdgPopup.zig"); + +/// The view this xdg toplevel implements +view: *View, + +/// The corresponding wlroots object +wlr_xdg_surface: *c.wlr_xdg_surface, + +// Listeners that are always active over the view's lifetime +listen_destroy: c.wl_listener, +listen_map: c.wl_listener, +listen_unmap: c.wl_listener, + +// Listeners that are only active while the view is mapped +listen_commit: c.wl_listener, +listen_new_popup: c.wl_listener, + +pub fn init(self: *Self, view: *View, wlr_xdg_surface: *c.wlr_xdg_surface) void { + self.view = view; + self.wlr_xdg_surface = wlr_xdg_surface; + wlr_xdg_surface.data = self; + + // Inform the xdg toplevel that it is tiled. + // For example this prevents firefox from drawing shadows around itself + _ = c.wlr_xdg_toplevel_set_tiled(self.wlr_xdg_surface, c.WLR_EDGE_LEFT | + c.WLR_EDGE_RIGHT | c.WLR_EDGE_TOP | c.WLR_EDGE_BOTTOM); + + // Add listeners that are active over the view's entire lifetime + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&self.wlr_xdg_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&self.wlr_xdg_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&self.wlr_xdg_surface.events.unmap, &self.listen_unmap); +} + +pub fn configure(self: Self, pending_box: Box) void { + self.view.pending_serial = c.wlr_xdg_toplevel_set_size( + self.wlr_xdg_surface, + pending_box.width, + pending_box.height, + ); +} + +pub fn setActivated(self: Self, activated: bool) void { + _ = c.wlr_xdg_toplevel_set_activated(self.wlr_xdg_surface, activated); +} + +/// Close the view. This will lead to the unmap and destroy events being sent +pub fn close(self: Self) void { + c.wlr_xdg_toplevel_send_close(self.wlr_xdg_surface); +} + +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + c.wlr_xdg_surface_for_each_surface(self.wlr_xdg_surface, iterator, user_data); +} + +/// Return the surface at output coordinates ox, oy and set sx, sy to the +/// corresponding surface-relative coordinates, if there is a surface. +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + return c.wlr_xdg_surface_surface_at( + self.wlr_xdg_surface, + ox - @intToFloat(f64, self.view.current_box.x), + oy - @intToFloat(f64, self.view.current_box.y), + sx, + sy, + ); +} + +/// Called when the xdg surface is destroyed +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + const output = self.view.output; + + // Remove listeners that are active for the entire lifetime of the view + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_map.link); + c.wl_list_remove(&self.listen_unmap.link); + + self.view.destroy(); +} + +/// Called when the xdg surface is mapped, or ready to display on-screen. +fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_map", listener.?); + const view = self.view; + const root = view.output.root; + + // Add listeners that are only active while mapped + self.listen_commit.notify = handleCommit; + c.wl_signal_add(&self.wlr_xdg_surface.surface.*.events.commit, &self.listen_commit); + + self.listen_new_popup.notify = handleNewPopup; + c.wl_signal_add(&self.wlr_xdg_surface.events.new_popup, &self.listen_new_popup); + + view.wlr_surface = self.wlr_xdg_surface.surface; + view.floating = false; + + view.natural_width = @intCast(u32, self.wlr_xdg_surface.geometry.width); + view.natural_height = @intCast(u32, self.wlr_xdg_surface.geometry.height); + + if (view.natural_width == 0 and view.natural_height == 0) { + view.natural_width = @intCast(u32, self.wlr_xdg_surface.surface.*.current.width); + view.natural_height = @intCast(u32, self.wlr_xdg_surface.surface.*.current.height); + } + + const wlr_xdg_toplevel: *c.wlr_xdg_toplevel = @field( + self.wlr_xdg_surface, + c.wlr_xdg_surface_union, + ).toplevel; + const state = &wlr_xdg_toplevel.current; + const app_id: [*:0]const u8 = if (wlr_xdg_toplevel.app_id) |id| id else "NULL"; + + Log.Debug.log("View with app_id '{}' mapped", .{app_id}); + + for (root.server.config.float_filter.items) |filter_app_id| { + // Make views with app_ids listed in the float filter float + if (std.mem.eql(u8, std.mem.span(app_id), std.mem.span(filter_app_id))) { + view.setFloating(true); + break; + } + } else if ((wlr_xdg_toplevel.parent != null) or + (state.min_width != 0 and state.min_height != 0 and + (state.min_width == state.max_width or state.min_height == state.max_height))) + { + // If the toplevel has a parent or is of fixed size make it float + view.setFloating(true); + } + + view.map(); +} + +/// Called when the surface is unmapped and will no longer be displayed. +fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_unmap", listener.?); + const root = self.view.output.root; + + self.view.unmap(); + + // Remove listeners that are only active while mapped + c.wl_list_remove(&self.listen_commit.link); + c.wl_list_remove(&self.listen_new_popup.link); +} + +/// Called when the surface is comitted +/// TODO: check for unexpected change in size and react as needed +fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_commit", listener.?); + const view = self.view; + + if (view.pending_serial) |s| { + if (s == self.wlr_xdg_surface.configure_serial) { + view.output.root.notifyConfigured(); + view.pending_serial = null; + } + } +} + +/// Called when a new xdg popup is requested by the client +fn handleNewPopup(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_popup", listener.?); + const wlr_xdg_popup = @ptrCast(*c.wlr_xdg_popup, @alignCast(@alignOf(*c.wlr_xdg_popup), data)); + const allocator = self.view.output.root.server.allocator; + + // This will free itself on destroy + var xdg_popup = allocator.create(XdgPopup) catch unreachable; + xdg_popup.init(self.view.output, &self.view.current_box, wlr_xdg_popup); +} diff --git a/river/XwaylandUnmanaged.zig b/river/XwaylandUnmanaged.zig new file mode 100644 index 0000000..fe33e73 --- /dev/null +++ b/river/XwaylandUnmanaged.zig @@ -0,0 +1,136 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const Root = @import("Root.zig"); + +root: *Root, + +/// The corresponding wlroots object +wlr_xwayland_surface: *c.wlr_xwayland_surface, + +// Listeners that are always active over the view's lifetime +liseten_request_configure: c.wl_listener, +listen_destroy: c.wl_listener, +listen_map: c.wl_listener, +listen_unmap: c.wl_listener, + +// Listeners that are only active while the view is mapped +listen_commit: c.wl_listener, + +pub fn init(self: *Self, root: *Root, wlr_xwayland_surface: *c.wlr_xwayland_surface) void { + self.root = root; + self.wlr_xwayland_surface = wlr_xwayland_surface; + + // Add listeners that are active over the view's entire lifetime + self.liseten_request_configure.notify = handleRequestConfigure; + c.wl_signal_add(&wlr_xwayland_surface.events.request_configure, &self.liseten_request_configure); + + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&wlr_xwayland_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&wlr_xwayland_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&wlr_xwayland_surface.events.unmap, &self.listen_unmap); +} + +/// Return the surface at output coordinates ox, oy and set sx, sy to the +/// corresponding surface-relative coordinates, if there is a surface. +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + return c.wlr_surface_surface_at( + self.wlr_xwayland_surface.surface, + ox - @intToFloat(f64, self.view.current_box.x), + oy - @intToFloat(f64, self.view.current_box.y), + sx, + sy, + ); +} + +fn handleRequestConfigure(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "liseten_request_configure", listener.?); + const wlr_xwayland_surface_configure_event = @ptrCast( + *c.wlr_xwayland_surface_configure_event, + @alignCast(@alignOf(*c.wlr_xwayland_surface_configure_event), data), + ); + c.wlr_xwayland_surface_configure( + self.wlr_xwayland_surface, + wlr_xwayland_surface_configure_event.x, + wlr_xwayland_surface_configure_event.y, + wlr_xwayland_surface_configure_event.width, + wlr_xwayland_surface_configure_event.height, + ); +} + +/// Called when the xwayland surface is destroyed +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + + // Remove listeners that are active for the entire lifetime of the view + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_map.link); + c.wl_list_remove(&self.listen_unmap.link); + + // Deallocate the node + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.root.server.allocator.destroy(node); +} + +/// Called when the xwayland surface is mapped, or ready to display on-screen. +fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_map", listener.?); + const root = self.root; + + // Add self to the list of unmanaged views in the root + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + root.xwayland_unmanaged_views.prepend(node); + + // Add listeners that are only active while mapped + self.listen_commit.notify = handleCommit; + c.wl_signal_add(&self.wlr_xwayland_surface.surface.*.events.commit, &self.listen_commit); + + // TODO: handle keyboard focus + // if (wlr_xwayland_or_surface_wants_focus(self.wlr_xwayland_surface)) { ... +} + +/// Called when the surface is unmapped and will no longer be displayed. +fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_unmap", listener.?); + + // Remove self from the list of unmanged views in the root + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.root.xwayland_unmanaged_views.remove(node); + + // Remove listeners that are only active while mapped + c.wl_list_remove(&self.listen_commit.link); + + // TODO: return focus +} + +/// Called when the surface is comitted +fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_commit", listener.?); + // TODO: check if the surface has moved for damage tracking +} diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig new file mode 100644 index 0000000..dbd35e4 --- /dev/null +++ b/river/XwaylandView.zig @@ -0,0 +1,159 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XdgPopup = @import("XdgPopup.zig"); + +/// The view this xwayland view implements +view: *View, + +/// The corresponding wlroots object +wlr_xwayland_surface: *c.wlr_xwayland_surface, + +// Listeners that are always active over the view's lifetime +listen_destroy: c.wl_listener, +listen_map: c.wl_listener, +listen_unmap: c.wl_listener, + +// Listeners that are only active while the view is mapped +listen_commit: c.wl_listener, + +pub fn init(self: *Self, view: *View, wlr_xwayland_surface: *c.wlr_xwayland_surface) void { + self.view = view; + self.wlr_xwayland_surface = wlr_xwayland_surface; + wlr_xwayland_surface.data = self; + + // Add listeners that are active over the view's entire lifetime + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&self.wlr_xwayland_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&self.wlr_xwayland_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&self.wlr_xwayland_surface.events.unmap, &self.listen_unmap); +} + +/// Tell the client to take a new size +pub fn configure(self: Self, pending_box: Box) void { + c.wlr_xwayland_surface_configure( + self.wlr_xwayland_surface, + @intCast(i16, pending_box.x), + @intCast(i16, pending_box.y), + @intCast(u16, pending_box.width), + @intCast(u16, pending_box.height), + ); + // Xwayland surfaces don't use serials, so we will just assume they have + // configured the next time they commit. Set pending serial to a dummy + // value to indicate that a transaction has started. Note: we can't just + // call notifyConfigured() here as the transaction has not yet been fully + // initiated. + self.view.pending_serial = 0x66666666; +} + +/// Inform the xwayland surface that it has gained focus +pub fn setActivated(self: Self, activated: bool) void { + c.wlr_xwayland_surface_activate(self.wlr_xwayland_surface, activated); +} + +/// Close the view. This will lead to the unmap and destroy events being sent +pub fn close(self: Self) void { + c.wlr_xwayland_surface_close(self.wlr_xwayland_surface); +} + +/// Iterate over all surfaces of the xwayland view. +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + c.wlr_surface_for_each_surface(self.wlr_xwayland_surface.surface, iterator, user_data); +} + +/// Return the surface at output coordinates ox, oy and set sx, sy to the +/// corresponding surface-relative coordinates, if there is a surface. +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + return c.wlr_surface_surface_at( + self.wlr_xwayland_surface.surface, + ox - @intToFloat(f64, self.view.current_box.x), + oy - @intToFloat(f64, self.view.current_box.y), + sx, + sy, + ); +} + +/// Called when the xwayland surface is destroyed +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + + // Remove listeners that are active for the entire lifetime of the view + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_map.link); + c.wl_list_remove(&self.listen_unmap.link); + + self.view.destroy(); +} + +/// Called when the xwayland surface is mapped, or ready to display on-screen. +fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_map", listener.?); + const view = self.view; + const root = view.output.root; + + // Add listeners that are only active while mapped + self.listen_commit.notify = handleCommit; + c.wl_signal_add(&self.wlr_xwayland_surface.surface.*.events.commit, &self.listen_commit); + + view.wlr_surface = self.wlr_xwayland_surface.surface; + view.floating = false; + + view.natural_width = self.wlr_xwayland_surface.width; + view.natural_height = self.wlr_xwayland_surface.height; + + view.map(); +} + +/// Called when the surface is unmapped and will no longer be displayed. +fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_unmap", listener.?); + + self.view.unmap(); + + // Remove listeners that are only active while mapped + c.wl_list_remove(&self.listen_commit.link); +} + +/// Called when the surface is comitted +/// TODO: check for unexpected change in size and react as needed +fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_commit", listener.?); + const view = self.view; + // See comment in XwaylandView.configure() + if (view.pending_serial != null) { + view.output.root.notifyConfigured(); + view.pending_serial = null; + } +} diff --git a/river/c.zig b/river/c.zig new file mode 100644 index 0000000..df4bcee --- /dev/null +++ b/river/c.zig @@ -0,0 +1,60 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pub usingnamespace @cImport({ + @cDefine("_POSIX_C_SOURCE", "200809L"); + @cDefine("WLR_USE_UNSTABLE", {}); + + @cInclude("time.h"); + @cInclude("stdlib.h"); + @cInclude("wayland-server-core.h"); + //@cInclude("wlr/backend.h"); + //@cInclude("wlr/render/wlr_renderer.h"); + @cInclude("wlr/types/wlr_buffer.h"); + @cInclude("wlr/types/wlr_compositor.h"); + @cInclude("wlr/types/wlr_cursor.h"); + @cInclude("wlr/types/wlr_data_device.h"); + @cInclude("wlr/types/wlr_input_device.h"); + @cInclude("wlr/types/wlr_input_inhibitor.h"); + @cInclude("wlr/types/wlr_keyboard.h"); + @cInclude("wlr/types/wlr_layer_shell_v1.h"); + @cInclude("wlr/types/wlr_matrix.h"); + @cInclude("wlr/types/wlr_output.h"); + @cInclude("wlr/types/wlr_output_layout.h"); + @cInclude("wlr/types/wlr_pointer.h"); + @cInclude("wlr/types/wlr_screencopy_v1.h"); + @cInclude("wlr/types/wlr_seat.h"); + @cInclude("wlr/types/wlr_xcursor_manager.h"); + @cInclude("wlr/types/wlr_xdg_decoration_v1.h"); + @cInclude("wlr/types/wlr_xdg_output_v1.h"); + @cInclude("wlr/types/wlr_xdg_shell.h"); + if (@import("build_options").xwayland) @cInclude("wlr/xwayland.h"); + @cInclude("wlr/util/log.h"); + @cInclude("xkbcommon/xkbcommon.h"); + + // Contains a subset of functions from wlr/backend.h and wlr/render/wlr_renderer.h + // that can be automatically imported + @cInclude("include/bindings.h"); + + @cInclude("river-control-unstable-v1-protocol.h"); +}); + +// These are needed because zig currently names translated anonymous unions +// with a global counter, which makes code unportable. +// See https://github.com/ifreund/river/issues/17 +pub const wlr_xdg_surface_union = @typeInfo(wlr_xdg_surface).Struct.fields[5].name; +pub const wlr_input_device_union = @typeInfo(wlr_input_device).Struct.fields[8].name; diff --git a/river/command.zig b/river/command.zig new file mode 100644 index 0000000..c1db559 --- /dev/null +++ b/river/command.zig @@ -0,0 +1,122 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Seat = @import("Seat.zig"); + +const impl = struct { + const close = @import("command/close.zig").close; + const declareMode = @import("command/declare_mode.zig").declareMode; + const enterMode = @import("command/enter_mode.zig").enterMode; + const exit = @import("command/exit.zig").exit; + const focus = @import("command/focus.zig").focus; + const focusAllTags = @import("command/focus_all_tags.zig").focusAllTags; + const focusOutput = @import("command/focus_output.zig").focusOutput; + const map = @import("command/map.zig").map; + const focusTag = @import("command/focus_tag.zig").focusTag; + const layout = @import("command/layout.zig").layout; + const modMasterCount = @import("command/mod_master_count.zig").modMasterCount; + const modMasterFactor = @import("command/mod_master_factor.zig").modMasterFactor; + const sendToOutput = @import("command/send_to_output.zig").sendToOutput; + const spawn = @import("command/spawn.zig").spawn; + const tagView = @import("command/tag_view.zig").tagView; + const tagViewAllTags = @import("command/tag_view_all_tags.zig").tagViewAllTags; + const toggleFloat = @import("command/toggle_float.zig").toggleFloat; + const toggleTagFocus = @import("command/toggle_tag_focus.zig").toggleTagFocus; + const toggleViewTag = @import("command/toggle_view_tag.zig").toggleViewTag; + const zoom = @import("command/zoom.zig").zoom; +}; + +pub const Direction = enum { + Next, + Prev, + + pub fn parse(str: []const u8) error{InvalidDirection}!Direction { + return if (std.mem.eql(u8, str, "next")) + Direction.Next + else if (std.mem.eql(u8, str, "previous")) + Direction.Prev + else + error.InvalidDirection; + } +}; + +const Definition = struct { + name: []const u8, + impl: fn (*std.mem.Allocator, *Seat, []const []const u8, *[]const u8) Error!void, +}; + +// TODO: this could be replaced with a comptime hashmap +// zig fmt: off +const str_to_impl_fn = [_]Definition{ + .{ .name = "close", .impl = impl.close }, + .{ .name = "declare_mode", .impl = impl.declareMode}, + .{ .name = "enter_mode", .impl = impl.enterMode }, + .{ .name = "exit", .impl = impl.exit }, + .{ .name = "focus", .impl = impl.focus }, + .{ .name = "focus_all_tags", .impl = impl.focusAllTags }, + .{ .name = "focus_output", .impl = impl.focusOutput }, + .{ .name = "focus_tag", .impl = impl.focusTag }, + .{ .name = "layout", .impl = impl.layout }, + .{ .name = "mod_master_count", .impl = impl.modMasterCount }, + .{ .name = "mod_master_factor", .impl = impl.modMasterFactor }, + .{ .name = "send_to_output", .impl = impl.sendToOutput }, + .{ .name = "spawn", .impl = impl.spawn }, + .{ .name = "map", .impl = impl.map }, + .{ .name = "tag_view", .impl = impl.tagView }, + .{ .name = "tag_view_all_tags", .impl = impl.tagViewAllTags }, + .{ .name = "toggle_float", .impl = impl.toggleFloat }, + .{ .name = "toggle_tag_focus", .impl = impl.toggleTagFocus }, + .{ .name = "toggle_view_tag", .impl = impl.toggleViewTag }, + .{ .name = "zoom", .impl = impl.zoom }, +}; +// zig fmt: on + +pub const Error = error{ + NoCommand, + UnknownCommand, + NotEnoughArguments, + TooManyArguments, + Overflow, + InvalidCharacter, + InvalidDirection, + OutOfMemory, + CommandFailed, +}; + +/// Run a command for the given Seat. The `args` parameter is similar to the +/// classic argv in that the command to be run is passed as the first argument. +/// If the command fails with Error.CommandFailed, a failure message will be +/// allocated and the slice pointed to by the `failure_message` parameter will +/// be set to point to it. The caller is responsible for freeing this message +/// in the case of failure. +pub fn run( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len == 0) return Error.NoCommand; + + const name = args[0]; + const impl_fn = for (str_to_impl_fn) |definition| { + if (std.mem.eql(u8, name, definition.name)) break definition.impl; + } else return Error.UnknownCommand; + + try impl_fn(allocator, seat, args, failure_message); +} diff --git a/river/command/close.zig b/river/command/close.zig new file mode 100644 index 0000000..04b2dcd --- /dev/null +++ b/river/command/close.zig @@ -0,0 +1,37 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Close the focused view, if any. +pub fn close( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (seat.focused_view) |view| { + // Note: we don't call arrange() here as it will be called + // automatically when the view is unmapped. + view.close(); + } +} diff --git a/river/command/declare_mode.zig b/river/command/declare_mode.zig new file mode 100644 index 0000000..943ede8 --- /dev/null +++ b/river/command/declare_mode.zig @@ -0,0 +1,51 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Mapping = @import("../Mapping.zig"); +const Seat = @import("../Seat.zig"); + +/// Declare a new keymap mode +pub fn declareMode( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const config = &seat.input_manager.server.config; + const new_mode_name = args[1]; + + if (config.mode_to_id.get(new_mode_name) != null) { + failure_message.* = try std.fmt.allocPrint( + allocator, + "mode '{}' already exists and cannot be re-declared", + .{new_mode_name}, + ); + return Error.CommandFailed; + } + + try config.mode_to_id.putNoClobber(new_mode_name, config.modes.items.len); + errdefer _ = config.mode_to_id.remove(new_mode_name); + try config.modes.append(std.ArrayList(Mapping).init(allocator)); +} diff --git a/river/command/enter_mode.zig b/river/command/enter_mode.zig new file mode 100644 index 0000000..8c7e3e4 --- /dev/null +++ b/river/command/enter_mode.zig @@ -0,0 +1,43 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Switch to the given mode +pub fn enterMode( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const config = seat.input_manager.server.config; + const target_mode = args[1]; + seat.mode_id = config.mode_to_id.getValue(target_mode) orelse { + failure_message.* = try std.fmt.allocPrint( + allocator, + "cannot enter non-existant mode '{}'", + .{target_mode}, + ); + return Error.CommandFailed; + }; +} diff --git a/river/command/exit.zig b/river/command/exit.zig new file mode 100644 index 0000000..a21e4f3 --- /dev/null +++ b/river/command/exit.zig @@ -0,0 +1,34 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Exit the compositor, terminating the wayland session. +pub fn exit( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len > 1) return Error.TooManyArguments; + c.wl_display_terminate(seat.input_manager.server.wl_display); +} diff --git a/river/command/focus.zig b/river/command/focus.zig new file mode 100644 index 0000000..0e2c9e0 --- /dev/null +++ b/river/command/focus.zig @@ -0,0 +1,67 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Direction = @import("../command.zig").Direction; +const Seat = @import("../Seat.zig"); +const View = @import("../View.zig"); +const ViewStack = @import("../view_stack.zig").ViewStack; + +/// Focus either the next or the previous visible view, depending on the enum +/// passed. Does nothing if there are 1 or 0 views in the stack. +pub fn focus( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const direction = try Direction.parse(args[1]); + const output = seat.focused_output; + + if (seat.focused_view) |current_focus| { + // If there is a currently focused view, focus the next visible view in the stack. + const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", current_focus); + var it = switch (direction) { + .Next => ViewStack(View).iterator(focused_node, output.current_focused_tags), + .Prev => ViewStack(View).reverseIterator(focused_node, output.current_focused_tags), + }; + + // Skip past the focused node + _ = it.next(); + // Focus the next visible node if there is one + if (it.next()) |node| { + seat.focus(&node.view); + return; + } + } + + // There is either no currently focused view or the last visible view in the + // stack is focused and we need to wrap. + var it = switch (direction) { + .Next => ViewStack(View).iterator(output.views.first, output.current_focused_tags), + .Prev => ViewStack(View).reverseIterator(output.views.last, output.current_focused_tags), + }; + + seat.focus(if (it.next()) |node| &node.view else null); +} diff --git a/river/command/focus_all_tags.zig b/river/command/focus_all_tags.zig new file mode 100644 index 0000000..28e001d --- /dev/null +++ b/river/command/focus_all_tags.zig @@ -0,0 +1,32 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Set focus to all tags +pub fn focusAllTags( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + seat.focused_output.pending_focused_tags = 0xFFFFFFFF; + seat.input_manager.server.root.arrange(); +} diff --git a/river/command/focus_output.zig b/river/command/focus_output.zig new file mode 100644 index 0000000..b11664f --- /dev/null +++ b/river/command/focus_output.zig @@ -0,0 +1,54 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Direction = @import("../command.zig").Direction; +const Output = @import("../Output.zig"); +const Seat = @import("../Seat.zig"); + +/// Focus either the next or the previous output, depending on the bool passed. +/// Does nothing if there is only one output. +pub fn focusOutput( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const direction = try Direction.parse(args[1]); + const root = &seat.input_manager.server.root; + // If the noop output is focused, there are no other outputs to switch to + if (seat.focused_output == &root.noop_output) { + std.debug.assert(root.outputs.len == 0); + return; + } + + // Focus the next/prev output in the list if there is one, else wrap + const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output); + seat.focused_output = switch (direction) { + .Next => if (focused_node.next) |node| &node.data else &root.outputs.first.?.data, + .Prev => if (focused_node.prev) |node| &node.data else &root.outputs.last.?.data, + }; + + seat.focus(null); +} diff --git a/river/command/focus_tag.zig b/river/command/focus_tag.zig new file mode 100644 index 0000000..8f69e6e --- /dev/null +++ b/river/command/focus_tag.zig @@ -0,0 +1,37 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Switch focus to the passed tag. +pub fn focusTag( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const tag = try std.fmt.parseInt(u32, args[1], 10); + const tags = @as(u32, 1) << @intCast(u5, tag - 1); + seat.focused_output.pending_focused_tags = tags; + seat.input_manager.server.root.arrange(); +} diff --git a/river/command/layout.zig b/river/command/layout.zig new file mode 100644 index 0000000..4a21050 --- /dev/null +++ b/river/command/layout.zig @@ -0,0 +1,37 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Leon Henrik Plickat +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +pub fn layout( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + seat.focused_output.layout = seat.focused_output.getLayoutByName(args[1]); + seat.focused_output.arrangeViews(); + seat.input_manager.server.root.startTransaction(); +} diff --git a/river/command/map.zig b/river/command/map.zig new file mode 100644 index 0000000..90c0b33 --- /dev/null +++ b/river/command/map.zig @@ -0,0 +1,110 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Mapping = @import("../Mapping.zig"); +const Seat = @import("../Seat.zig"); + +const modifier_names = [_]struct { + name: []const u8, + modifier: u32, +}{ + .{ .name = "Shift", .modifier = c.WLR_MODIFIER_SHIFT }, + .{ .name = "Lock", .modifier = c.WLR_MODIFIER_CAPS }, + .{ .name = "Control", .modifier = c.WLR_MODIFIER_CTRL }, + .{ .name = "Mod1", .modifier = c.WLR_MODIFIER_ALT }, + .{ .name = "Mod2", .modifier = c.WLR_MODIFIER_MOD2 }, + .{ .name = "Mod3", .modifier = c.WLR_MODIFIER_MOD3 }, + .{ .name = "Mod4", .modifier = c.WLR_MODIFIER_LOGO }, + .{ .name = "Mod5", .modifier = c.WLR_MODIFIER_MOD5 }, +}; + +/// Create a new mapping for a given mode +/// +/// Example: +/// map normal Mod4|Shift Return spawn alacritty +pub fn map( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 4) return Error.NotEnoughArguments; + + // Parse the mode + const config = seat.input_manager.server.config; + const target_mode = args[1]; + const mode_id = config.mode_to_id.getValue(target_mode) orelse { + failure_message.* = try std.fmt.allocPrint( + allocator, + "cannot add mapping to non-existant mode '{}p'", + .{target_mode}, + ); + return Error.CommandFailed; + }; + + // Parse the modifiers + var it = std.mem.split(args[2], "|"); + var modifiers: u32 = 0; + while (it.next()) |mod_name| { + for (modifier_names) |def| { + if (std.mem.eql(u8, def.name, mod_name)) { + modifiers |= def.modifier; + break; + } + } else { + failure_message.* = try std.fmt.allocPrint( + allocator, + "invalid modifier '{}'", + .{mod_name}, + ); + return Error.CommandFailed; + } + } + + // Parse the keysym + const keysym_name = try std.cstr.addNullByte(allocator, args[3]); + defer allocator.free(keysym_name); + const keysym = c.xkb_keysym_from_name(keysym_name, .XKB_KEYSYM_CASE_INSENSITIVE); + if (keysym == c.XKB_KEY_NoSymbol) { + failure_message.* = try std.fmt.allocPrint( + allocator, + "invalid keysym '{}'", + .{args[3]}, + ); + return Error.CommandFailed; + } + + // Check if the mapping already exists + const mode_mappings = &config.modes.items[mode_id]; + for (mode_mappings.items) |existant_mapping| { + if (existant_mapping.modifiers == modifiers and existant_mapping.keysym == keysym) { + failure_message.* = try std.fmt.allocPrint( + allocator, + "a mapping for modifiers '{}' and keysym '{}' already exists", + .{ args[2], args[3] }, + ); + return Error.CommandFailed; + } + } + + try mode_mappings.append(try Mapping.init(allocator, keysym, modifiers, args[4..])); +} diff --git a/river/command/mod_master_count.zig b/river/command/mod_master_count.zig new file mode 100644 index 0000000..38a379e --- /dev/null +++ b/river/command/mod_master_count.zig @@ -0,0 +1,42 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Modify the number of master views +pub fn modMasterCount( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const delta = try std.fmt.parseInt(i32, args[1], 10); + const output = seat.focused_output; + output.master_count = @intCast( + u32, + std.math.max(0, @intCast(i32, output.master_count) + delta), + ); + seat.input_manager.server.root.arrange(); +} diff --git a/river/command/mod_master_factor.zig b/river/command/mod_master_factor.zig new file mode 100644 index 0000000..ec8065a --- /dev/null +++ b/river/command/mod_master_factor.zig @@ -0,0 +1,45 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Modify the percent of the width of the screen that the master views occupy. +pub fn modMasterFactor( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const delta = try std.fmt.parseFloat(f64, args[1]); + const output = seat.focused_output; + const new_master_factor = std.math.min( + std.math.max(output.master_factor + delta, 0.05), + 0.95, + ); + if (new_master_factor != output.master_factor) { + output.master_factor = new_master_factor; + seat.input_manager.server.root.arrange(); + } +} diff --git a/river/command/send_to_output.zig b/river/command/send_to_output.zig new file mode 100644 index 0000000..de92f6f --- /dev/null +++ b/river/command/send_to_output.zig @@ -0,0 +1,62 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Direction = @import("../command.zig").Direction; +const Output = @import("../Output.zig"); +const Seat = @import("../Seat.zig"); + +/// Send the focused view to the the next or the previous output, depending on +/// the bool passed. Does nothing if there is only one output. +pub fn sendToOutput( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const direction = try Direction.parse(args[1]); + const root = &seat.input_manager.server.root; + + if (seat.focused_view) |view| { + // If the noop output is focused, there is nowhere to send the view + if (view.output == &root.noop_output) { + std.debug.assert(root.outputs.len == 0); + return; + } + + // Send to the next/preg output in the list if there is one, else wrap + const current_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", view.output); + const destination_output = switch (direction) { + .Next => if (current_node.next) |node| &node.data else &root.outputs.first.?.data, + .Prev => if (current_node.prev) |node| &node.data else &root.outputs.last.?.data, + }; + + // Move the view to the target output + view.sendToOutput(destination_output); + + // Handle the change and focus whatever's next in the focus stack + root.arrange(); + seat.focus(null); + } +} diff --git a/river/command/spawn.zig b/river/command/spawn.zig new file mode 100644 index 0000000..880483a --- /dev/null +++ b/river/command/spawn.zig @@ -0,0 +1,47 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Spawn a program. +pub fn spawn( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + + const cmd = try std.mem.join(allocator, " ", args[1..]); + defer allocator.free(cmd); + + const child_args = [_][]const u8{ "/bin/sh", "-c", cmd }; + const child = try std.ChildProcess.init(&child_args, allocator); + defer child.deinit(); + + std.ChildProcess.spawn(child) catch |err| { + failure_message.* = try std.fmt.allocPrint( + allocator, + "failed to spawn {}: {}.", + .{ cmd, err }, + ); + return Error.CommandFailed; + }; +} diff --git a/river/command/tag_view.zig b/river/command/tag_view.zig new file mode 100644 index 0000000..735481d --- /dev/null +++ b/river/command/tag_view.zig @@ -0,0 +1,43 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Set the tag of the focused view. +pub fn tagView( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const tag = try std.fmt.parseInt(u32, args[1], 10); + const tags = @as(u32, 1) << @intCast(u5, tag - 1); + if (seat.focused_view) |view| { + if (view.current_tags != tags) { + view.pending_tags = tags; + seat.input_manager.server.root.arrange(); + } + } +} diff --git a/river/command/tag_view_all_tags.zig b/river/command/tag_view_all_tags.zig new file mode 100644 index 0000000..2d187f3 --- /dev/null +++ b/river/command/tag_view_all_tags.zig @@ -0,0 +1,38 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Tag the focused view with all tags. +pub fn tagViewAllTags( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (seat.focused_view) |view| { + if (view.current_tags != 0xFFFFFFFF) { + view.pending_tags = 0xFFFFFFFF; + seat.input_manager.server.root.arrange(); + } + } +} diff --git a/river/command/toggle_float.zig b/river/command/toggle_float.zig new file mode 100644 index 0000000..25655e8 --- /dev/null +++ b/river/command/toggle_float.zig @@ -0,0 +1,38 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Make the focused view float or stop floating, depending on its current +/// state. +pub fn toggleFloat( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len > 1) return Error.TooManyArguments; + if (seat.focused_view) |view| { + view.setFloating(!view.floating); + view.output.root.arrange(); + } +} diff --git a/river/command/toggle_tag_focus.zig b/river/command/toggle_tag_focus.zig new file mode 100644 index 0000000..09761c7 --- /dev/null +++ b/river/command/toggle_tag_focus.zig @@ -0,0 +1,43 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Toggle focus of the passsed tags. +pub fn toggleTagFocus( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const tag = try std.fmt.parseInt(u32, args[1], 10); + const tags = @as(u32, 1) << @intCast(u5, tag - 1); + const output = seat.focused_output; + const new_focused_tags = output.current_focused_tags ^ tags; + if (new_focused_tags != 0) { + output.pending_focused_tags = new_focused_tags; + seat.input_manager.server.root.arrange(); + } +} diff --git a/river/command/toggle_view_tag.zig b/river/command/toggle_view_tag.zig new file mode 100644 index 0000000..469a750 --- /dev/null +++ b/river/command/toggle_view_tag.zig @@ -0,0 +1,44 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); + +/// Toggle the passed tag of the focused view +pub fn toggleViewTag( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const tag = try std.fmt.parseInt(u32, args[1], 10); + const tags = @as(u32, 1) << @intCast(u5, tag - 1); + if (seat.focused_view) |view| { + const new_tags = view.current_tags ^ tags; + if (new_tags != 0) { + view.pending_tags = new_tags; + seat.input_manager.server.root.arrange(); + } + } +} diff --git a/river/command/zoom.zig b/river/command/zoom.zig new file mode 100644 index 0000000..59156ab --- /dev/null +++ b/river/command/zoom.zig @@ -0,0 +1,54 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("../c.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); +const View = @import("../View.zig"); +const ViewStack = @import("../view_stack.zig").ViewStack; + +/// Bump the focused view to the top of the stack. If the view on the top of +/// the stack is focused, bump the second view to the top. +pub fn zoom( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + failure_message: *[]const u8, +) Error!void { + if (args.len > 1) return Error.TooManyArguments; + + if (seat.focused_view) |current_focus| { + const output = seat.focused_output; + const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", current_focus); + + var it = ViewStack(View).iterator(output.views.first, output.current_focused_tags); + const zoom_node = if (focused_node == it.next()) + if (it.next()) |second| second else null + else + focused_node; + + if (zoom_node) |to_bump| { + output.views.remove(to_bump); + output.views.push(to_bump); + seat.input_manager.server.root.arrange(); + seat.focus(&to_bump.view); + } + } +} diff --git a/river/log.zig b/river/log.zig new file mode 100644 index 0000000..30d2564 --- /dev/null +++ b/river/log.zig @@ -0,0 +1,41 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +pub const Log = enum { + const Self = @This(); + + Silent = 0, + Error = 1, + Info = 2, + Debug = 3, + + var verbosity = Self.Error; + + pub fn init(_verbosity: Self) void { + verbosity = _verbosity; + } + + fn log(level: Self, comptime format: []const u8, args: var) void { + if (@enumToInt(level) <= @enumToInt(verbosity)) { + // TODO: log the time since start in the same format as wlroots + // TODO: use color if logging to a tty + std.debug.warn("[{}] " ++ format ++ "\n", .{@tagName(level)} ++ args); + } + } +}; diff --git a/river/main.zig b/river/main.zig new file mode 100644 index 0000000..a5f5334 --- /dev/null +++ b/river/main.zig @@ -0,0 +1,42 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Server = @import("Server.zig"); + +pub fn main() !void { + Log.init(Log.Debug); + c.wlr_log_init(.WLR_ERROR, null); + + Log.Info.log("Initializing server", .{}); + + var server: Server = undefined; + try server.init(std.heap.c_allocator); + defer server.deinit(); + + try server.start(); + + Log.Info.log("Running server...", .{}); + + server.run(); + + Log.Info.log("Shutting down server", .{}); +} diff --git a/river/render.zig b/river/render.zig new file mode 100644 index 0000000..a7055ef --- /dev/null +++ b/river/render.zig @@ -0,0 +1,315 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const build_options = @import("build_options"); +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const LayerSurface = @import("LayerSurface.zig"); +const Output = @import("Output.zig"); +const Server = @import("Server.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +const SurfaceRenderData = struct { + output: *const Output, + + /// In output layout coordinates relative to the output + output_x: i32, + output_y: i32, + + when: *c.timespec, +}; + +pub fn renderOutput(output: *Output) void { + const wlr_renderer = output.getRenderer(); + + var now: c.timespec = undefined; + _ = c.clock_gettime(c.CLOCK_MONOTONIC, &now); + + // wlr_output_attach_render makes the OpenGL context current. + if (!c.wlr_output_attach_render(output.wlr_output, null)) { + return; + } + // The "effective" resolution can change if you rotate your outputs. + var width: c_int = undefined; + var height: c_int = undefined; + c.wlr_output_effective_resolution(output.wlr_output, &width, &height); + // Begin the renderer (calls glViewport and some other GL sanity checks) + c.wlr_renderer_begin(wlr_renderer, width, height); + + const color = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }; + c.wlr_renderer_clear(wlr_renderer, &color); + + renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND], &now); + renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM], &now); + + // The first view in the list is "on top" so iterate in reverse. + var it = ViewStack(View).reverseIterator(output.views.last, output.current_focused_tags); + while (it.next()) |node| { + const view = &node.view; + // This check prevents a race condition when a frame is requested + // between mapping of a view and the first configure being handled. + if (view.current_box.width == 0 or view.current_box.height == 0) { + continue; + } + // Floating views are rendered on top of normal views + if (view.floating) { + continue; + } + renderView(output.*, view, &now); + renderBorders(output.*, view, &now); + } + + // Render floating views + it = ViewStack(View).reverseIterator(output.views.last, output.current_focused_tags); + while (it.next()) |node| { + const view = &node.view; + // This check prevents a race condition when a frame is requested + // between mapping of a view and the first configure being handled. + if (view.current_box.width == 0 or view.current_box.height == 0) { + continue; + } + if (!view.floating) { + continue; + } + renderView(output.*, view, &now); + renderBorders(output.*, view, &now); + } + + // Render xwayland unmanged views + if (build_options.xwayland) { + renderXwaylandUnmanaged(output.*, &now); + } + + renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_TOP], &now); + renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY], &now); + + // Hardware cursors are rendered by the GPU on a separate plane, and can be + // moved around without re-rendering what's beneath them - which is more + // efficient. However, not all hardware supports hardware cursors. For this + // reason, wlroots provides a software fallback, which we ask it to render + // here. wlr_cursor handles configuring hardware vs software cursors for you, + // and this function is a no-op when hardware cursors are in use. + c.wlr_output_render_software_cursors(output.wlr_output, null); + + // Conclude rendering and swap the buffers, showing the final frame + // on-screen. + c.wlr_renderer_end(wlr_renderer); + // TODO: handle failure + _ = c.wlr_output_commit(output.wlr_output); +} + +/// Render all surfaces on the passed layer +fn renderLayer(output: Output, layer: std.TailQueue(LayerSurface), now: *c.timespec) void { + var it = layer.first; + while (it) |node| : (it = node.next) { + const layer_surface = &node.data; + var rdata = SurfaceRenderData{ + .output = &output, + .output_x = layer_surface.box.x, + .output_y = layer_surface.box.y, + .when = now, + }; + c.wlr_layer_surface_v1_for_each_surface( + layer_surface.wlr_layer_surface, + renderSurface, + &rdata, + ); + } +} + +fn renderView(output: Output, view: *View, now: *c.timespec) void { + // If we have a stashed buffer, we are in the middle of a transaction + // and need to render that buffer until the transaction is complete. + if (view.stashed_buffer) |buffer| { + var box = c.wlr_box{ + .x = view.current_box.x, + .y = view.current_box.y, + .width = @intCast(c_int, view.current_box.width), + .height = @intCast(c_int, view.current_box.height), + }; + + // Scale the box to the output's current scaling factor + scaleBox(&box, output.wlr_output.scale); + + var matrix: [9]f32 = undefined; + c.wlr_matrix_project_box( + &matrix, + &box, + .WL_OUTPUT_TRANSFORM_NORMAL, + 0.0, + &output.wlr_output.transform_matrix, + ); + + // This takes our matrix, the texture, and an alpha, and performs the actual + // rendering on the GPU. + _ = c.wlr_render_texture_with_matrix( + output.getRenderer(), + buffer.texture, + &matrix, + 1.0, + ); + } else { + // Since there is no stashed buffer, we are not in the middle of + // a transaction and may simply render each toplevel surface. + var rdata = SurfaceRenderData{ + .output = &output, + .output_x = view.current_box.x, + .output_y = view.current_box.y, + .when = now, + }; + + view.forEachSurface(renderSurface, &rdata); + } +} + +/// Render all xwayland unmanaged windows that appear on the output +fn renderXwaylandUnmanaged(output: Output, now: *c.timespec) void { + const root = output.root; + const output_box: *c.wlr_box = c.wlr_output_layout_get_box( + root.wlr_output_layout, + output.wlr_output, + ); + + var it = output.root.xwayland_unmanaged_views.first; + while (it) |node| : (it = node.next) { + const wlr_xwayland_surface = node.data.wlr_xwayland_surface; + + var rdata = SurfaceRenderData{ + .output = &output, + .output_x = wlr_xwayland_surface.x - output_box.x, + .output_y = wlr_xwayland_surface.y - output_box.y, + .when = now, + }; + c.wlr_surface_for_each_surface(wlr_xwayland_surface.surface, renderSurface, &rdata); + } +} + +/// This function is passed to wlroots to render each surface during iteration +fn renderSurface( + _surface: ?*c.wlr_surface, + surface_x: c_int, + surface_y: c_int, + data: ?*c_void, +) callconv(.C) void { + // wlroots says this will never be null + const surface = _surface.?; + const rdata = @ptrCast(*SurfaceRenderData, @alignCast(@alignOf(SurfaceRenderData), data)); + const output = rdata.output; + const wlr_output = output.wlr_output; + + // We first obtain a wlr_texture, which is a GPU resource. wlroots + // automatically handles negotiating these with the client. The underlying + // resource could be an opaque handle passed from the client, or the client + // could have sent a pixel buffer which we copied to the GPU, or a few other + // means. You don't have to worry about this, wlroots takes care of it. + const texture = c.wlr_surface_get_texture(surface); + if (texture == null) { + return; + } + + var box = c.wlr_box{ + .x = rdata.output_x + surface_x, + .y = rdata.output_y + surface_y, + .width = surface.current.width, + .height = surface.current.height, + }; + + // Scale the box to the output's current scaling factor + scaleBox(&box, wlr_output.scale); + + // wlr_matrix_project_box is a helper which takes a box with a desired + // x, y coordinates, width and height, and an output geometry, then + // prepares an orthographic projection and multiplies the necessary + // transforms to produce a model-view-projection matrix. + var matrix: [9]f32 = undefined; + const transform = c.wlr_output_transform_invert(surface.current.transform); + c.wlr_matrix_project_box(&matrix, &box, transform, 0.0, &wlr_output.transform_matrix); + + // This takes our matrix, the texture, and an alpha, and performs the actual + // rendering on the GPU. + _ = c.wlr_render_texture_with_matrix(output.getRenderer(), texture, &matrix, 1.0); + + // This lets the client know that we've displayed that frame and it can + // prepare another one now if it likes. + c.wlr_surface_send_frame_done(surface, rdata.when); +} + +fn renderBorders(output: Output, view: *View, now: *c.timespec) void { + var border: Box = undefined; + const color = if (view.focused) + [_]f32{ 0.57647059, 0.63137255, 0.63137255, 1.0 } // Solarized base1 + else + [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }; // Solarized base01 + const border_width = output.root.server.config.border_width; + + // left and right, covering the corners as well + border.y = view.current_box.y - @intCast(i32, border_width); + border.width = border_width; + border.height = view.current_box.height + border_width * 2; + + // left + border.x = view.current_box.x - @intCast(i32, border_width); + renderRect(output, border, color); + + // right + border.x = view.current_box.x + @intCast(i32, view.current_box.width); + renderRect(output, border, color); + + // top and bottom + border.x = view.current_box.x; + border.width = view.current_box.width; + border.height = border_width; + + // top + border.y = view.current_box.y - @intCast(i32, border_width); + renderRect(output, border, color); + + // bottom border + border.y = view.current_box.y + @intCast(i32, view.current_box.height); + renderRect(output, border, color); +} + +fn renderRect(output: Output, box: Box, color: [4]f32) void { + var wlr_box = box.toWlrBox(); + scaleBox(&wlr_box, output.wlr_output.scale); + c.wlr_render_rect( + output.getRenderer(), + &wlr_box, + &color, + &output.wlr_output.transform_matrix, + ); +} + +/// Scale a wlr_box, taking the possibility of fractional scaling into account. +fn scaleBox(box: *c.wlr_box, scale: f64) void { + box.x = @floatToInt(c_int, @round(@intToFloat(f64, box.x) * scale)); + box.y = @floatToInt(c_int, @round(@intToFloat(f64, box.y) * scale)); + box.width = scaleLength(box.width, box.x, scale); + box.height = scaleLength(box.height, box.x, scale); +} + +/// Scales a width/height. +/// +/// This might seem overly complex, but it needs to work for fractional scaling. +fn scaleLength(length: c_int, offset: c_int, scale: f64) c_int { + return @floatToInt(c_int, @round(@intToFloat(f64, offset + length) * scale) - + @round(@intToFloat(f64, offset) * scale)); +} diff --git a/river/test_main.zig b/river/test_main.zig new file mode 100644 index 0000000..2ed8247 --- /dev/null +++ b/river/test_main.zig @@ -0,0 +1,20 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +test "river test suite" { + _ = @import("view_stack.zig"); +} diff --git a/river/view_stack.zig b/river/view_stack.zig new file mode 100644 index 0000000..0543d03 --- /dev/null +++ b/river/view_stack.zig @@ -0,0 +1,406 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const View = @import("View.zig"); + +/// A specialized doubly-linked stack that allows for filtered iteration +/// over the nodes. T must be View or *View. +pub fn ViewStack(comptime T: type) type { + if (!(T == View or T == *View)) { + @compileError("ViewStack: T must be View or *View"); + } + return struct { + const Self = @This(); + + pub const Node = struct { + /// Previous/next nodes in the stack + prev: ?*Node, + next: ?*Node, + + /// The view stored in this node + view: T, + }; + + /// Top/bottom nodes in the stack + first: ?*Node, + last: ?*Node, + + /// Initialize an undefined stack + pub fn init(self: *Self) void { + self.first = null; + self.last = null; + } + + /// Add a node to the top of the stack. + pub fn push(self: *Self, new_node: *Node) void { + // Set the prev/next pointers of the new node + new_node.prev = null; + new_node.next = self.first; + + if (self.first) |first| { + // If the list is not empty, set the prev pointer of the current + // first node to the new node. + first.prev = new_node; + } else { + // If the list is empty set the last pointer to the new node. + self.last = new_node; + } + + // Set the first pointer to the new node + self.first = new_node; + } + + /// Remove a node from the view stack. This removes it from the stack of + /// all views as well as the stack of visible ones. + pub fn remove(self: *Self, target_node: *Node) void { + // Set the previous node/list head to the next pointer + if (target_node.prev) |prev_node| { + prev_node.next = target_node.next; + } else { + self.first = target_node.next; + } + + // Set the next node/list tail to the previous pointer + if (target_node.next) |next_node| { + next_node.prev = target_node.prev; + } else { + self.last = target_node.prev; + } + } + + const Iterator = struct { + it: ?*Node, + tags: u32, + reverse: bool, + pending: bool, + + /// Returns the next node in iteration order, or null if done. + /// This function is horribly ugly, but it's well tested below. + pub fn next(self: *Iterator) ?*Node { + while (self.it) |node| : (self.it = if (self.reverse) node.prev else node.next) { + if (if (self.pending) + if (node.view.pending_tags) |pending_tags| + self.tags & pending_tags != 0 + else + self.tags & node.view.current_tags != 0 + else + self.tags & node.view.current_tags != 0) { + self.it = if (self.reverse) node.prev else node.next; + return node; + } + } + return null; + } + }; + + /// Returns an iterator starting at the passed node and filtered by + /// checking the passed tags against the current tags of each view. + pub fn iterator(start: ?*Node, tags: u32) Iterator { + return Iterator{ + .it = start, + .tags = tags, + .reverse = false, + .pending = false, + }; + } + + /// Returns a reverse iterator starting at the passed node and filtered by + /// checking the passed tags against the current tags of each view. + pub fn reverseIterator(start: ?*Node, tags: u32) Iterator { + return Iterator{ + .it = start, + .tags = tags, + .reverse = true, + .pending = false, + }; + } + + /// Returns an iterator starting at the passed node and filtered by + /// checking the passed tags against the pending tags of each view. + /// If a view has no pending tags, the current tags are used. + pub fn pendingIterator(start: ?*Node, tags: u32) Iterator { + return Iterator{ + .it = start, + .tags = tags, + .reverse = false, + .pending = true, + }; + } + }; +} + +test "push/remove (*View)" { + const testing = @import("std").testing; + + const allocator = testing.allocator; + + var views: ViewStack(*View) = undefined; + views.init(); + + const one = try allocator.create(ViewStack(*View).Node); + defer allocator.destroy(one); + const two = try allocator.create(ViewStack(*View).Node); + defer allocator.destroy(two); + const three = try allocator.create(ViewStack(*View).Node); + defer allocator.destroy(three); + const four = try allocator.create(ViewStack(*View).Node); + defer allocator.destroy(four); + const five = try allocator.create(ViewStack(*View).Node); + defer allocator.destroy(five); + + views.push(three); // {3} + views.push(one); // {1, 3} + views.push(four); // {4, 1, 3} + views.push(five); // {5, 4, 1, 3} + views.push(two); // {2, 5, 4, 1, 3} + + // Simple insertion + { + var it = views.first; + testing.expect(it == two); + it = it.?.next; + testing.expect(it == five); + it = it.?.next; + testing.expect(it == four); + it = it.?.next; + testing.expect(it == one); + it = it.?.next; + testing.expect(it == three); + it = it.?.next; + + testing.expect(it == null); + + testing.expect(views.first == two); + testing.expect(views.last == three); + } + + // Removal of first + views.remove(two); + { + var it = views.first; + testing.expect(it == five); + it = it.?.next; + testing.expect(it == four); + it = it.?.next; + testing.expect(it == one); + it = it.?.next; + testing.expect(it == three); + it = it.?.next; + + testing.expect(it == null); + + testing.expect(views.first == five); + testing.expect(views.last == three); + } + + // Removal of last + views.remove(three); + { + var it = views.first; + testing.expect(it == five); + it = it.?.next; + testing.expect(it == four); + it = it.?.next; + testing.expect(it == one); + it = it.?.next; + + testing.expect(it == null); + + testing.expect(views.first == five); + testing.expect(views.last == one); + } + + // Remove from middle + views.remove(four); + { + var it = views.first; + testing.expect(it == five); + it = it.?.next; + testing.expect(it == one); + it = it.?.next; + + testing.expect(it == null); + + testing.expect(views.first == five); + testing.expect(views.last == one); + } + + // Reinsertion + views.push(two); + views.push(three); + views.push(four); + { + var it = views.first; + testing.expect(it == four); + it = it.?.next; + testing.expect(it == three); + it = it.?.next; + testing.expect(it == two); + it = it.?.next; + testing.expect(it == five); + it = it.?.next; + testing.expect(it == one); + it = it.?.next; + + testing.expect(it == null); + + testing.expect(views.first == four); + testing.expect(views.last == one); + } + + // Clear + views.remove(four); + views.remove(two); + views.remove(three); + views.remove(one); + views.remove(five); + + testing.expect(views.first == null); + testing.expect(views.last == null); +} + +test "iteration (View)" { + const c = @import("c.zig"); + const testing = @import("std").testing; + + const allocator = testing.allocator; + + var views: ViewStack(View) = undefined; + views.init(); + + const one_a_pb = try allocator.create(ViewStack(View).Node); + defer allocator.destroy(one_a_pb); + one_a_pb.view.current_tags = 1 << 0; + one_a_pb.view.pending_tags = 1 << 1; + + const two_a = try allocator.create(ViewStack(View).Node); + defer allocator.destroy(two_a); + two_a.view.current_tags = 1 << 0; + two_a.view.pending_tags = null; + + const three_b_pa = try allocator.create(ViewStack(View).Node); + defer allocator.destroy(three_b_pa); + three_b_pa.view.current_tags = 1 << 1; + three_b_pa.view.pending_tags = 1 << 0; + + const four_b = try allocator.create(ViewStack(View).Node); + defer allocator.destroy(four_b); + four_b.view.current_tags = 1 << 1; + four_b.view.pending_tags = null; + + const five_b = try allocator.create(ViewStack(View).Node); + defer allocator.destroy(five_b); + five_b.view.current_tags = 1 << 1; + five_b.view.pending_tags = null; + + views.push(three_b_pa); // {3} + views.push(one_a_pb); // {1, 3} + views.push(four_b); // {4, 1, 3} + views.push(five_b); // {5, 4, 1, 3} + views.push(two_a); // {2, 5, 4, 1, 3} + + // Iteration over all tags + { + var it = ViewStack(View).iterator(views.first, 0xFFFFFFFF); + testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); + testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); + testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); + testing.expect(it.next() == null); + } + + // Iteration over 'a' tags + { + var it = ViewStack(View).iterator(views.first, 1 << 0); + testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); + testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); + testing.expect(it.next() == null); + } + + // Iteration over 'b' tags + { + var it = ViewStack(View).iterator(views.first, 1 << 1); + testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); + testing.expect(it.next() == null); + } + + // Iteration over tags that aren't present + { + var it = ViewStack(View).iterator(views.first, 1 << 2); + testing.expect(it.next() == null); + } + + // Reverse iteration over all tags + { + var it = ViewStack(View).reverseIterator(views.last, 0xFFFFFFFF); + testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); + testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); + testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); + testing.expect(it.next() == null); + } + + // Reverse iteration over 'a' tags + { + var it = ViewStack(View).reverseIterator(views.last, 1 << 0); + testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); + testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); + testing.expect(it.next() == null); + } + + // Reverse iteration over 'b' tags + { + var it = ViewStack(View).reverseIterator(views.last, 1 << 1); + testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); + testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); + testing.expect(it.next() == null); + } + + // Reverse iteration over tags that aren't present + { + var it = ViewStack(View).reverseIterator(views.first, 1 << 2); + testing.expect(it.next() == null); + } + + // Iteration over (pending) 'a' tags + { + var it = ViewStack(View).pendingIterator(views.first, 1 << 0); + testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); + testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); + testing.expect(it.next() == null); + } + + // Iteration over (pending) 'b' tags + { + var it = ViewStack(View).pendingIterator(views.first, 1 << 1); + testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); + testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); + testing.expect(it.next() == null); + } + + // Iteration over (pending) tags that aren't present + { + var it = ViewStack(View).pendingIterator(views.first, 1 << 2); + testing.expect(it.next() == null); + } +} diff --git a/riverctl/main.zig b/riverctl/main.zig new file mode 100644 index 0000000..287c1aa --- /dev/null +++ b/riverctl/main.zig @@ -0,0 +1,109 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const c = @cImport({ + @cInclude("wayland-client.h"); + @cInclude("river-control-unstable-v1-client-protocol.h"); +}); + +const wl_registry_listener = c.wl_registry_listener{ + .global = handleGlobal, + .global_remove = handleGlobalRemove, +}; + +const command_callback_listener = c.zriver_command_callback_v1_listener{ + .success = handleSuccess, + .failure = handleFailure, +}; + +var river_control_optional: ?*c.zriver_control_v1 = null; + +pub fn main() !void { + const wl_display = c.wl_display_connect(null) orelse return error.CantConnectToDisplay; + const wl_registry = c.wl_display_get_registry(wl_display); + + if (c.wl_registry_add_listener(wl_registry, &wl_registry_listener, null) < 0) + return error.FailedToAddListener; + if (c.wl_display_roundtrip(wl_display) < 0) return error.RoundtripFailed; + + const river_control = river_control_optional orelse return error.RiverControlNotAdvertised; + + var command: c.wl_array = undefined; + c.wl_array_init(&command); + var it = std.process.args(); + // Skip our name + _ = it.nextPosix(); + while (it.nextPosix()) |arg| { + // Add one as we need to copy the null terminators as well + var ptr = @ptrCast([*]u8, c.wl_array_add(&command, arg.len + 1) orelse + return error.OutOfMemory); + for (arg) |ch, i| ptr[i] = ch; + ptr[arg.len] = 0; + } + + const command_callback = c.zriver_control_v1_run_command(river_control, &command); + if (c.zriver_command_callback_v1_add_listener( + command_callback, + &command_callback_listener, + null, + ) < 0) return error.FailedToAddListener; + + // Loop until our callback is called and we exit. + while (true) if (c.wl_display_dispatch(wl_display) < 0) return error.DispatchFailed; +} + +fn handleGlobal( + data: ?*c_void, + wl_registry: ?*c.wl_registry, + name: u32, + interface: ?[*:0]const u8, + version: u32, +) callconv(.C) void { + // We only care about the river_control global + if (std.mem.eql( + u8, + std.mem.spanZ(interface.?), + std.mem.spanZ(@ptrCast([*:0]const u8, c.zriver_control_v1_interface.name.?)), + )) { + river_control_optional = @ptrCast( + *c.zriver_control_v1, + c.wl_registry_bind(wl_registry, name, &c.zriver_control_v1_interface, 1), + ); + } +} + +/// Ignore the event +fn handleGlobalRemove(data: ?*c_void, wl_registry: ?*c.wl_registry, name: u32) callconv(.C) void {} + +/// On success we simply exit with a clean exit code +fn handleSuccess(data: ?*c_void, callback: ?*c.zriver_command_callback_v1) callconv(.C) void { + std.os.exit(0); +} + +/// Print the failure message and exit non-zero +fn handleFailure( + data: ?*c_void, + callback: ?*c.zriver_command_callback_v1, + failure_message: ?[*:0]const u8, +) callconv(.C) void { + if (failure_message) |message| { + std.debug.warn("Error: {}\n", .{failure_message}); + } + std.os.exit(1); +} diff --git a/src/Box.zig b/src/Box.zig deleted file mode 100644 index 0ba3e63..0000000 --- a/src/Box.zig +++ /dev/null @@ -1,34 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const c = @import("c.zig"); - -x: i32, -y: i32, -width: u32, -height: u32, - -pub fn toWlrBox(self: Self) c.wlr_box { - return c.wlr_box{ - .x = @intCast(c_int, self.x), - .y = @intCast(c_int, self.y), - .width = @intCast(c_int, self.width), - .height = @intCast(c_int, self.height), - }; -} diff --git a/src/Config.zig b/src/Config.zig deleted file mode 100644 index 20b7504..0000000 --- a/src/Config.zig +++ /dev/null @@ -1,284 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Server = @import("Server.zig"); -const Mapping = @import("Mapping.zig"); - -/// Width of borders in pixels -border_width: u32, - -/// Amount of view padding in pixels -view_padding: u32, - -/// Amount of padding arount the outer edge of the layout in pixels -outer_padding: u32, - -/// Map of keymap mode name to mode id -mode_to_id: std.StringHashMap(usize), - -/// All user-defined keymap modes, indexed by mode id -modes: std.ArrayList(std.ArrayList(Mapping)), - -/// List of app_ids which will be started floating -float_filter: std.ArrayList([*:0]const u8), - -pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { - self.border_width = 2; - self.view_padding = 8; - self.outer_padding = 8; - - self.mode_to_id = std.StringHashMap(usize).init(allocator); - try self.mode_to_id.putNoClobber("normal", 0); - try self.mode_to_id.putNoClobber("passthrough", 1); - - self.modes = std.ArrayList(std.ArrayList(Mapping)).init(allocator); - try self.modes.append(std.ArrayList(Mapping).init(allocator)); - try self.modes.append(std.ArrayList(Mapping).init(allocator)); - - self.float_filter = std.ArrayList([*:0]const u8).init(allocator); - - const normal_keybinds = &self.modes.items[0]; - const mod = c.WLR_MODIFIER_LOGO; - - // Mod+Shift+Return to start an instance of alacritty - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_Return, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "spawn", "alacritty" }, - )); - - // Mod+Q to close the focused view - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_q, - mod, - &[_][]const u8{"close"}, - )); - - // Mod+E to exit river - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_e, - mod, - &[_][]const u8{"exit"}, - )); - - // Mod+J and Mod+K to focus the next/previous view in the layout stack - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_j, - mod, - &[_][]const u8{ "focus", "next" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_k, - mod, - &[_][]const u8{ "focus", "previous" }, - )); - - // Mod+Return to bump the focused view to the top of the layout stack, - // making it the new master - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_Return, - mod, - &[_][]const u8{"zoom"}, - )); - - // Mod+H and Mod+L to increase/decrease the width of the master column - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_h, - mod, - &[_][]const u8{ "mod_master_factor", "+0.05" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_l, - mod, - &[_][]const u8{ "mod_master_factor", "-0.05" }, - )); - - // Mod+Shift+H and Mod+Shift+L to increment/decrement the number of - // master views in the layout - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_h, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "mod_master_count", "+1" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_l, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "mod_master_count", "+1" }, - )); - - comptime var i = 0; - inline while (i < 9) : (i += 1) { - const str = &[_]u8{i + '0' + 1}; - // Mod+[1-9] to focus tag [1-9] - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_1 + i, - mod, - &[_][]const u8{ "focus_tag", str }, - )); - // Mod+Shift+[1-9] to tag focused view with tag [1-9] - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_1 + i, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "tag_view", str }, - )); - // Mod+Ctrl+[1-9] to toggle focus of tag [1-9] - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_1 + i, - mod | c.WLR_MODIFIER_CTRL, - &[_][]const u8{ "toggle_tag_focus", str }, - )); - // Mod+Shift+Ctrl+[1-9] to toggle tag [1-9] of focused view - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_1 + i, - mod | c.WLR_MODIFIER_CTRL | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "toggle_view_tag", str }, - )); - } - - // Mod+0 to focus all tags - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_0, - mod, - &[_][]const u8{"focus_all_tags"}, - )); - - // Mod+Shift+0 to tag focused view with all tags - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_0, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{"tag_view_all_tags"}, - )); - - // Mod+Period and Mod+Comma to focus the next/previous output - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_period, - mod, - &[_][]const u8{ "focus_output", "next" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_comma, - mod, - &[_][]const u8{ "focus_output", "previous" }, - )); - - // Mod+Shift+Period/Comma to send the focused view to the the - // next/previous output - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_period, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "send_to_output", "next" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_comma, - mod | c.WLR_MODIFIER_SHIFT, - &[_][]const u8{ "send_to_output", "previous" }, - )); - - // Mod+Space to toggle float - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_space, - mod, - &[_][]const u8{"toggle_float"}, - )); - - // Mod+F11 to enter passthrough mode - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_F11, - mod, - &[_][]const u8{ "enter_mode", "passthrough" }, - )); - - // Change master orientation with Mod+{Up,Right,Down,Left} - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_Up, - mod, - &[_][]const u8{ "layout", "TopMaster" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_Right, - mod, - &[_][]const u8{ "layout", "RightMaster" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_Down, - mod, - &[_][]const u8{ "layout", "BottomMaster" }, - )); - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_Left, - mod, - &[_][]const u8{ "layout", "LeftMaster" }, - )); - - // Mod+f to change to Full layout - try normal_keybinds.append(try Mapping.init( - allocator, - c.XKB_KEY_f, - mod, - &[_][]const u8{ "layout", "Full" }, - )); - - // Mod+F11 to return to normal mode - try self.modes.items[1].append(try Mapping.init( - allocator, - c.XKB_KEY_F11, - mod, - &[_][]const u8{ "enter_mode", "normal" }, - )); - - // Float views with app_id "float" - try self.float_filter.append("float"); -} - -pub fn deinit(self: Self, allocator: *std.mem.Allocator) void { - self.mode_to_id.deinit(); - for (self.modes.items) |*mode| mode.deinit(); - self.modes.deinit(); -} diff --git a/src/Control.zig b/src/Control.zig deleted file mode 100644 index e9ae576..0000000 --- a/src/Control.zig +++ /dev/null @@ -1,139 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); -const command = @import("command.zig"); - -const Log = @import("log.zig").Log; -const Server = @import("Server.zig"); - -const protocol_version = 1; - -const implementation = c.struct_zriver_control_v1_interface{ - .run_command = runCommand, -}; - -server: *Server, -wl_global: *c.wl_global, - -listen_display_destroy: c.wl_listener, - -pub fn init(self: *Self, server: *Server) !void { - self.server = server; - self.wl_global = c.wl_global_create( - server.wl_display, - &c.zriver_control_v1_interface, - protocol_version, - self, - bind, - ) orelse return error.CantCreateRiverWindowManagementGlobal; - - self.listen_display_destroy.notify = handleDisplayDestroy; - c.wl_display_add_destroy_listener(server.wl_display, &self.listen_display_destroy); -} - -fn handleDisplayDestroy(wl_listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_display_destroy", wl_listener.?); - c.wl_global_destroy(self.wl_global); -} - -/// Called when a client binds our global -fn bind(wl_client: ?*c.wl_client, data: ?*c_void, version: u32, id: u32) callconv(.C) void { - const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); - const wl_resource = c.wl_resource_create( - wl_client, - &c.zriver_control_v1_interface, - @intCast(c_int, version), - id, - ) orelse { - c.wl_client_post_no_memory(wl_client); - return; - }; - c.wl_resource_set_implementation(wl_resource, &implementation, self, resourceDestroy); -} - -fn resourceDestroy(wl_resource: ?*c.wl_resource) callconv(.C) void { - // TODO -} - -fn runCommand( - wl_client: ?*c.wl_client, - wl_resource: ?*c.wl_resource, - wl_array: ?*c.wl_array, - callback_id: u32, -) callconv(.C) void { - const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource))); - const allocator = self.server.allocator; - const seat = self.server.input_manager.default_seat; - - var args = std.ArrayList([]const u8).init(allocator); - - var i: usize = 0; - const data = @ptrCast([*]const u8, wl_array.?.data); - while (i < wl_array.?.size) { - const slice = std.mem.spanZ(@ptrCast([*:0]const u8, &data[i])); - args.append(std.mem.dupe(allocator, u8, slice) catch unreachable) catch unreachable; - - i += slice.len + 1; - } - - const callback_resource = c.wl_resource_create( - wl_client, - &c.zriver_command_callback_v1_interface, - protocol_version, - callback_id, - ) orelse { - c.wl_client_post_no_memory(wl_client); - return; - }; - - c.wl_resource_set_implementation(callback_resource, null, null, null); - - var failure_message: []const u8 = undefined; - command.run(allocator, seat, args.items, &failure_message) catch |err| { - if (err == command.Error.CommandFailed) { - defer allocator.free(failure_message); - const out = std.cstr.addNullByte(allocator, failure_message) catch { - c.zriver_command_callback_v1_send_failure(callback_resource, "out of memory"); - return; - }; - defer allocator.free(out); - c.zriver_command_callback_v1_send_failure(callback_resource, out); - } else { - c.zriver_command_callback_v1_send_failure( - callback_resource, - switch (err) { - command.Error.NoCommand => "no command given", - command.Error.UnknownCommand => "unknown command", - command.Error.NotEnoughArguments => "not enough arguments", - command.Error.TooManyArguments => "too many arguments", - command.Error.Overflow => "value out of bounds", - command.Error.InvalidCharacter => "invalid character in argument", - command.Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'", - command.Error.OutOfMemory => "out of memory", - command.Error.CommandFailed => unreachable, - }, - ); - } - return; - }; - c.zriver_command_callback_v1_send_success(callback_resource); -} diff --git a/src/Cursor.zig b/src/Cursor.zig deleted file mode 100644 index 9eede57..0000000 --- a/src/Cursor.zig +++ /dev/null @@ -1,412 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const build_options = @import("build_options"); -const std = @import("std"); - -const c = @import("c.zig"); - -const LayerSurface = @import("LayerSurface.zig"); -const Log = @import("log.zig").Log; -const Output = @import("Output.zig"); -const Seat = @import("Seat.zig"); -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; - -const CursorMode = enum { - Passthrough, - Move, - Resize, -}; - -seat: *Seat, -wlr_cursor: *c.wlr_cursor, -wlr_xcursor_manager: *c.wlr_xcursor_manager, - -mode: CursorMode, -grabbed_view: ?*View, -grab_x: f64, -grab_y: f64, -grab_width: c_int, -grab_height: c_int, -resize_edges: u32, - -listen_axis: c.wl_listener, -listen_button: c.wl_listener, -listen_frame: c.wl_listener, -listen_motion_absolute: c.wl_listener, -listen_motion: c.wl_listener, -listen_request_set_cursor: c.wl_listener, - -pub fn init(self: *Self, seat: *Seat) !void { - self.seat = seat; - - // Creates a wlroots utility for tracking the cursor image shown on screen. - self.wlr_cursor = c.wlr_cursor_create() orelse - return error.CantCreateWlrCursor; - - // Creates an xcursor manager, another wlroots utility which loads up - // Xcursor themes to source cursor images from and makes sure that cursor - // images are available at all scale factors on the screen (necessary for - // HiDPI support). We add a cursor theme at scale factor 1 to begin with. - self.wlr_xcursor_manager = c.wlr_xcursor_manager_create(null, 24) orelse - return error.CantCreateWlrXCursorManager; - c.wlr_cursor_attach_output_layout(self.wlr_cursor, seat.input_manager.server.root.wlr_output_layout); - if (c.wlr_xcursor_manager_load(self.wlr_xcursor_manager, 1) == 0) { - if (build_options.xwayland) { - if (c.wlr_xcursor_manager_get_xcursor( - self.wlr_xcursor_manager, - "left_ptr", - 1, - )) |wlr_xcursor| { - const image: *c.wlr_xcursor_image = wlr_xcursor.*.images[0]; - c.wlr_xwayland_set_cursor( - seat.input_manager.server.wlr_xwayland, - image.buffer, - image.width * 4, - image.width, - image.height, - @intCast(i32, image.hotspot_x), - @intCast(i32, image.hotspot_y), - ); - } - } - } else { - Log.Error.log("Failed to load an xcursor theme", .{}); - } - - self.mode = CursorMode.Passthrough; - self.grabbed_view = null; - self.grab_x = 0.0; - self.grab_y = 0.0; - self.grab_width = 0; - self.grab_height = 0; - self.resize_edges = 0; - - // wlr_cursor *only* displays an image on screen. It does not move around - // when the pointer moves. However, we can attach input devices to it, and - // it will generate aggregate events for all of them. In these events, we - // can choose how we want to process them, forwarding them to clients and - // moving the cursor around. See following post for more detail: - // https://drewdevault.com/2018/07/17/Input-handling-in-wlroots.html - self.listen_axis.notify = handleAxis; - c.wl_signal_add(&self.wlr_cursor.events.axis, &self.listen_axis); - - self.listen_button.notify = handleButton; - c.wl_signal_add(&self.wlr_cursor.events.button, &self.listen_button); - - self.listen_frame.notify = handleFrame; - c.wl_signal_add(&self.wlr_cursor.events.frame, &self.listen_frame); - - self.listen_motion_absolute.notify = handleMotionAbsolute; - c.wl_signal_add(&self.wlr_cursor.events.motion_absolute, &self.listen_motion_absolute); - - self.listen_motion.notify = handleMotion; - c.wl_signal_add(&self.wlr_cursor.events.motion, &self.listen_motion); - - self.listen_request_set_cursor.notify = handleRequestSetCursor; - c.wl_signal_add(&self.seat.wlr_seat.events.request_set_cursor, &self.listen_request_set_cursor); -} - -pub fn deinit(self: *Self) void { - c.wlr_xcursor_manager_destroy(self.wlr_xcursor_manager); - c.wlr_cursor_destroy(self.wlr_cursor); -} - -fn handleAxis(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is forwarded by the cursor when a pointer emits an axis event, - // for example when you move the scroll wheel. - const cursor = @fieldParentPtr(Self, "listen_axis", listener.?); - const event = @ptrCast( - *c.wlr_event_pointer_axis, - @alignCast(@alignOf(*c.wlr_event_pointer_axis), data), - ); - - // Notify the client with pointer focus of the axis event. - c.wlr_seat_pointer_notify_axis( - cursor.seat.wlr_seat, - event.time_msec, - event.orientation, - event.delta, - event.delta_discrete, - event.source, - ); -} - -fn handleButton(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is forwarded by the cursor when a pointer emits a button - // event. - const self = @fieldParentPtr(Self, "listen_button", listener.?); - const event = @ptrCast( - *c.wlr_event_pointer_button, - @alignCast(@alignOf(*c.wlr_event_pointer_button), data), - ); - var sx: f64 = undefined; - var sy: f64 = undefined; - - if (self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy)) |wlr_surface| { - // If the found surface is a keyboard inteactive layer surface, - // give it keyboard focus. - if (c.wlr_surface_is_layer_surface(wlr_surface)) { - const wlr_layer_surface = c.wlr_layer_surface_v1_from_wlr_surface(wlr_surface); - if (wlr_layer_surface.*.current.keyboard_interactive) { - const layer_surface = @ptrCast( - *LayerSurface, - @alignCast(@alignOf(*LayerSurface), wlr_layer_surface.*.data), - ); - self.seat.setFocusRaw(.{ .layer = layer_surface }); - } - } - - // If the found surface is an xdg toplevel surface, send keyboard - // focus to the view. - if (c.wlr_surface_is_xdg_surface(wlr_surface)) { - const wlr_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(wlr_surface); - if (wlr_xdg_surface.*.role == .WLR_XDG_SURFACE_ROLE_TOPLEVEL) { - const view = @ptrCast(*View, @alignCast(@alignOf(*View), wlr_xdg_surface.*.data)); - self.seat.focus(view); - } - } - - _ = c.wlr_seat_pointer_notify_button( - self.seat.wlr_seat, - event.time_msec, - event.button, - event.state, - ); - } -} - -fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is forwarded by the cursor when a pointer emits an frame - // event. Frame events are sent after regular pointer events to group - // multiple events together. For instance, two axis events may happen at the - // same time, in which case a frame event won't be sent in between. - const self = @fieldParentPtr(Self, "listen_frame", listener.?); - // Notify the client with pointer focus of the frame event. - c.wlr_seat_pointer_notify_frame(self.seat.wlr_seat); -} - -fn handleMotionAbsolute(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is forwarded by the cursor when a pointer emits an _absolute_ - // motion event, from 0..1 on each axis. This happens, for example, when - // wlroots is running under a Wayland window rather than KMS+DRM, and you - // move the mouse over the window. You could enter the window from any edge, - // so we have to warp the mouse there. There is also some hardware which - // emits these events. - const self = @fieldParentPtr(Self, "listen_motion_absolute", listener.?); - const event = @ptrCast( - *c.wlr_event_pointer_motion_absolute, - @alignCast(@alignOf(*c.wlr_event_pointer_motion_absolute), data), - ); - c.wlr_cursor_warp_absolute(self.wlr_cursor, event.device, event.x, event.y); - self.processMotion(event.time_msec); -} - -fn handleMotion(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is forwarded by the cursor when a pointer emits a _relative_ - // pointer motion event (i.e. a delta) - const self = @fieldParentPtr(Self, "listen_motion", listener.?); - const event = @ptrCast( - *c.wlr_event_pointer_motion, - @alignCast(@alignOf(*c.wlr_event_pointer_motion), data), - ); - // The cursor doesn't move unless we tell it to. The cursor automatically - // handles constraining the motion to the output layout, as well as any - // special configuration applied for the specific input device which - // generated the event. You can pass NULL for the device if you want to move - // the cursor around without any input. - c.wlr_cursor_move(self.wlr_cursor, event.device, event.delta_x, event.delta_y); - self.processMotion(event.time_msec); -} - -fn handleRequestSetCursor(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is rasied by the seat when a client provides a cursor image - const self = @fieldParentPtr(Self, "listen_request_set_cursor", listener.?); - const event = @ptrCast( - *c.wlr_seat_pointer_request_set_cursor_event, - @alignCast(@alignOf(*c.wlr_seat_pointer_request_set_cursor_event), data), - ); - const focused_client = self.seat.wlr_seat.pointer_state.focused_client; - - // This can be sent by any client, so we check to make sure this one is - // actually has pointer focus first. - if (focused_client == event.seat_client) { - // Once we've vetted the client, we can tell the cursor to use the - // provided surface as the cursor image. It will set the hardware cursor - // on the output that it's currently on and continue to do so as the - // cursor moves between outputs. - Log.Debug.log("Focused client set cursor", .{}); - c.wlr_cursor_set_surface( - self.wlr_cursor, - event.surface, - event.hotspot_x, - event.hotspot_y, - ); - } -} - -fn processMotion(self: Self, time: u32) void { - var sx: f64 = undefined; - var sy: f64 = undefined; - if (self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy)) |wlr_surface| { - // "Enter" the surface if necessary. This lets the client know that the - // cursor has entered one of its surfaces. - // - // Note that this gives the surface "pointer focus", which is distinct - // from keyboard focus. You get pointer focus by moving the pointer over - // a window. - if (self.seat.input_manager.inputAllowed(wlr_surface)) { - const wlr_seat = self.seat.wlr_seat; - const focus_change = wlr_seat.pointer_state.focused_surface != wlr_surface; - if (focus_change) { - Log.Debug.log("Pointer notify enter at ({},{})", .{ sx, sy }); - c.wlr_seat_pointer_notify_enter(wlr_seat, wlr_surface, sx, sy); - } else { - // The enter event contains coordinates, so we only need to notify - // on motion if the focus did not change. - c.wlr_seat_pointer_notify_motion(wlr_seat, time, sx, sy); - } - return; - } - } - - // There is either no surface under the cursor or input is disallowed - // Reset the cursor image to the default - c.wlr_xcursor_manager_set_cursor_image( - self.wlr_xcursor_manager, - "left_ptr", - self.wlr_cursor, - ); - // Clear pointer focus so future button events and such are not sent to - // the last client to have the cursor over it. - c.wlr_seat_pointer_clear_focus(self.seat.wlr_seat); -} - -/// Find the topmost surface under the output layout coordinates lx/ly -/// returns the surface if found and sets the sx/sy parametes to the -/// surface coordinates. -fn surfaceAt(self: Self, lx: f64, ly: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { - // Find the output to check - const root = self.seat.input_manager.server.root; - const wlr_output = c.wlr_output_layout_output_at(root.wlr_output_layout, lx, ly) orelse - return null; - const output = @ptrCast( - *Output, - @alignCast(@alignOf(*Output), wlr_output.*.data orelse return null), - ); - - // Get output-local coords from the layout coords - var ox = lx; - var oy = ly; - c.wlr_output_layout_output_coords(root.wlr_output_layout, wlr_output, &ox, &oy); - - // Check layers and views from top to bottom - const layer_idxs = [_]usize{ - c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, - c.ZWLR_LAYER_SHELL_V1_LAYER_TOP, - c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, - c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, - }; - - // Check overlay layer incl. popups - if (layerSurfaceAt(output.*, output.layers[layer_idxs[0]], ox, oy, sx, sy, false)) |surface| { - return surface; - } - - // Check top-background popups only - for (layer_idxs[1..4]) |layer_idx| { - if (layerSurfaceAt(output.*, output.layers[layer_idx], ox, oy, sx, sy, true)) |surface| { - return surface; - } - } - - // Check top layer - if (layerSurfaceAt(output.*, output.layers[layer_idxs[1]], ox, oy, sx, sy, false)) |surface| { - return surface; - } - - // Check floating views then normal views - if (viewSurfaceAt(output.*, ox, oy, sx, sy, true)) |surface| { - return surface; - } - if (viewSurfaceAt(output.*, ox, oy, sx, sy, false)) |surface| { - return surface; - } - - // Check the bottom-background layers - for (layer_idxs[2..4]) |layer_idx| { - if (layerSurfaceAt(output.*, output.layers[layer_idx], ox, oy, sx, sy, false)) |surface| { - return surface; - } - } - - return null; -} - -/// Find the topmost surface on the given layer at ox,oy. Will only check -/// popups if popups_only is true. -fn layerSurfaceAt( - output: Output, - layer: std.TailQueue(LayerSurface), - ox: f64, - oy: f64, - sx: *f64, - sy: *f64, - popups_only: bool, -) ?*c.wlr_surface { - var it = layer.first; - while (it) |node| : (it = node.next) { - const layer_surface = &node.data; - const surface = c.wlr_layer_surface_v1_surface_at( - layer_surface.wlr_layer_surface, - ox - @intToFloat(f64, layer_surface.box.x), - oy - @intToFloat(f64, layer_surface.box.y), - sx, - sy, - ); - if (surface) |found| { - if (!popups_only) { - return found; - } else if (c.wlr_surface_is_xdg_surface(found)) { - const wlr_xdg_surface = c.wlr_xdg_surface_from_wlr_surface(found); - if (wlr_xdg_surface.*.role == .WLR_XDG_SURFACE_ROLE_POPUP) { - return found; - } - } - } - } - return null; -} - -/// Find the topmost visible view surface (incl. popups) at ox,oy. Will -/// check only floating views if floating is true. -fn viewSurfaceAt(output: Output, ox: f64, oy: f64, sx: *f64, sy: *f64, floating: bool) ?*c.wlr_surface { - var it = ViewStack(View).iterator(output.views.first, output.current_focused_tags); - while (it.next()) |node| { - const view = &node.view; - if (view.floating != floating) { - continue; - } - if (view.surfaceAt(ox, oy, sx, sy)) |found| { - return found; - } - } - return null; -} diff --git a/src/Decoration.zig b/src/Decoration.zig deleted file mode 100644 index 91c5bed..0000000 --- a/src/Decoration.zig +++ /dev/null @@ -1,53 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const DecorationManager = @import("DecorationManager.zig"); - -// TODO: this needs to listen for destroy and free nodes from the deco list -decoration_manager: *DecorationManager, -wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, - -listen_request_mode: c.wl_listener, - -pub fn init( - self: *Self, - decoration_manager: *DecorationManager, - wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, -) void { - self.decoration_manager = decoration_manager; - self.wlr_xdg_toplevel_decoration = wlr_xdg_toplevel_decoration; - - self.listen_request_mode.notify = handleRequestMode; - c.wl_signal_add(&self.wlr_xdg_toplevel_decoration.events.request_mode, &self.listen_request_mode); - - handleRequestMode(&self.listen_request_mode, self.wlr_xdg_toplevel_decoration); -} - -fn handleRequestMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_request_mode", listener.?); - // TODO: we might need to take this configure serial and do a transaction - _ = c.wlr_xdg_toplevel_decoration_v1_set_mode( - self.wlr_xdg_toplevel_decoration, - c.wlr_xdg_toplevel_decoration_v1_mode.WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE, - ); -} diff --git a/src/DecorationManager.zig b/src/DecorationManager.zig deleted file mode 100644 index 5a9abb4..0000000 --- a/src/DecorationManager.zig +++ /dev/null @@ -1,57 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Decoration = @import("Decoration.zig"); -const Server = @import("Server.zig"); - -server: *Server, - -wlr_xdg_decoration_manager: *c.wlr_xdg_decoration_manager_v1, - -decorations: std.SinglyLinkedList(Decoration), - -listen_new_toplevel_decoration: c.wl_listener, - -pub fn init(self: *Self, server: *Server) !void { - self.server = server; - self.wlr_xdg_decoration_manager = c.wlr_xdg_decoration_manager_v1_create(server.wl_display) orelse - return error.CantCreateWlrXdgDecorationManager; - - self.listen_new_toplevel_decoration.notify = handleNewToplevelDecoration; - c.wl_signal_add( - &self.wlr_xdg_decoration_manager.events.new_toplevel_decoration, - &self.listen_new_toplevel_decoration, - ); -} - -fn handleNewToplevelDecoration(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_toplevel_decoration", listener.?); - const wlr_xdg_toplevel_decoration = @ptrCast( - *c.wlr_xdg_toplevel_decoration_v1, - @alignCast(@alignOf(*c.wlr_xdg_toplevel_decoration_v1), data), - ); - - const node = self.decorations.allocateNode(self.server.allocator) catch unreachable; - node.data.init(self, wlr_xdg_toplevel_decoration); - self.decorations.prepend(node); -} diff --git a/src/InputManager.zig b/src/InputManager.zig deleted file mode 100644 index 3a95346..0000000 --- a/src/InputManager.zig +++ /dev/null @@ -1,146 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Seat = @import("Seat.zig"); -const Server = @import("Server.zig"); - -const default_seat_name = "default"; - -server: *Server, - -wlr_input_inhibit_manager: *c.wlr_input_inhibit_manager, - -seats: std.TailQueue(Seat), -default_seat: *Seat, - -exclusive_client: ?*c.wl_client, - -listen_inhibit_activate: c.wl_listener, -listen_inhibit_deactivate: c.wl_listener, -listen_new_input: c.wl_listener, - -pub fn init(self: *Self, server: *Server) !void { - self.server = server; - - // This is automatically freed when the display is destroyed - self.wlr_input_inhibit_manager = - c.wlr_input_inhibit_manager_create(server.wl_display) orelse - return error.CantCreateInputInhibitManager; - - self.seats = std.TailQueue(Seat).init(); - - const seat_node = try server.allocator.create(std.TailQueue(Seat).Node); - try seat_node.data.init(self, default_seat_name); - self.default_seat = &seat_node.data; - self.seats.prepend(seat_node); - - self.exclusive_client = null; - - // Set up all listeners - self.listen_inhibit_activate.notify = handleInhibitActivate; - c.wl_signal_add( - &self.wlr_input_inhibit_manager.events.activate, - &self.listen_inhibit_activate, - ); - - self.listen_inhibit_deactivate.notify = handleInhibitDeactivate; - c.wl_signal_add( - &self.wlr_input_inhibit_manager.events.deactivate, - &self.listen_inhibit_deactivate, - ); - - self.listen_new_input.notify = handleNewInput; - c.wl_signal_add(&self.server.wlr_backend.events.new_input, &self.listen_new_input); -} - -pub fn deinit(self: *Self) void { - while (self.seats.pop()) |seat_node| { - seat_node.data.deinit(); - self.server.allocator.destroy(seat_node); - } -} - -/// Must be called whenever a view is unmapped. -pub fn handleViewUnmap(self: Self, view: *View) void { - var it = self.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - seat.handleViewUnmap(view); - } -} - -/// Returns true if input is currently allowed on the passed surface. -pub fn inputAllowed(self: Self, wlr_surface: *c.wlr_surface) bool { - return if (self.exclusive_client) |exclusive_client| - exclusive_client == c.wl_resource_get_client(wlr_surface.resource) - else - true; -} - -fn handleInhibitActivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_inhibit_activate", listener.?); - - Log.Debug.log("Input inhibitor activated", .{}); - - // Clear focus of all seats - var seat_it = self.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - seat_node.data.setFocusRaw(.{ .none = {} }); - } - - self.exclusive_client = self.wlr_input_inhibit_manager.active_client; -} - -fn handleInhibitDeactivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_inhibit_deactivate", listener.?); - - Log.Debug.log("Input inhibitor deactivated", .{}); - - self.exclusive_client = null; - - // Calling arrangeLayers() like this ensures that any top or overlay, - // keyboard-interactive surfaces will re-grab focus. - var output_it = self.server.root.outputs.first; - while (output_it) |output_node| : (output_it = output_node.next) { - output_node.data.arrangeLayers(); - } - - // After ensuring that any possible layer surface focus grab has occured, - // have each Seat handle focus. - var seat_it = self.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - seat_node.data.focus(null); - } -} - -/// This event is raised by the backend when a new input device becomes available. -fn handleNewInput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_input", listener.?); - const device = @ptrCast(*c.wlr_input_device, @alignCast(@alignOf(*c.wlr_input_device), data)); - - // TODO: suport multiple seats - if (self.seats.first) |seat_node| { - seat_node.data.addDevice(device) catch unreachable; - } -} diff --git a/src/Keyboard.zig b/src/Keyboard.zig deleted file mode 100644 index cbfd6cd..0000000 --- a/src/Keyboard.zig +++ /dev/null @@ -1,180 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Seat = @import("Seat.zig"); - -seat: *Seat, -wlr_input_device: *c.wlr_input_device, -wlr_keyboard: *c.wlr_keyboard, - -listen_key: c.wl_listener, -listen_modifiers: c.wl_listener, - -pub fn init(self: *Self, seat: *Seat, wlr_input_device: *c.wlr_input_device) !void { - self.seat = seat; - self.wlr_input_device = wlr_input_device; - self.wlr_keyboard = @field(wlr_input_device, c.wlr_input_device_union).keyboard; - - // We need to prepare an XKB keymap and assign it to the keyboard. This - // assumes the defaults (e.g. layout = "us"). - const rules = c.xkb_rule_names{ - .rules = null, - .model = null, - .layout = null, - .variant = null, - .options = null, - }; - const context = c.xkb_context_new(.XKB_CONTEXT_NO_FLAGS) orelse - return error.CantCreateXkbContext; - defer c.xkb_context_unref(context); - - const keymap = c.xkb_keymap_new_from_names( - context, - &rules, - .XKB_KEYMAP_COMPILE_NO_FLAGS, - ) orelse - return error.CantCreateXkbKeymap; - defer c.xkb_keymap_unref(keymap); - - // TODO: handle failure after https://github.com/swaywm/wlroots/pull/2081 - c.wlr_keyboard_set_keymap(self.wlr_keyboard, keymap); - c.wlr_keyboard_set_repeat_info(self.wlr_keyboard, 25, 600); - - // Setup listeners for keyboard events - self.listen_key.notify = handleKey; - c.wl_signal_add(&self.wlr_keyboard.events.key, &self.listen_key); - - self.listen_modifiers.notify = handleModifiers; - c.wl_signal_add(&self.wlr_keyboard.events.modifiers, &self.listen_modifiers); -} - -fn handleKey(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when a key is pressed or released. - const self = @fieldParentPtr(Self, "listen_key", listener.?); - const event = @ptrCast( - *c.wlr_event_keyboard_key, - @alignCast(@alignOf(*c.wlr_event_keyboard_key), data), - ); - - const wlr_keyboard = self.wlr_keyboard; - - // Translate libinput keycode -> xkbcommon - const keycode = event.keycode + 8; - - // Get a list of keysyms as xkb reports them - var translated_keysyms: ?[*]c.xkb_keysym_t = undefined; - const translated_keysyms_len = c.xkb_state_key_get_syms( - wlr_keyboard.xkb_state, - keycode, - &translated_keysyms, - ); - - // Get a list of keysyms ignoring modifiers (e.g. 1 instead of !) - // Important for mappings like Mod+Shift+1 - var raw_keysyms: ?[*]c.xkb_keysym_t = undefined; - const layout_index = c.xkb_state_key_get_layout(wlr_keyboard.xkb_state, keycode); - const raw_keysyms_len = c.xkb_keymap_key_get_syms_by_level( - wlr_keyboard.keymap, - keycode, - layout_index, - 0, - &raw_keysyms, - ); - - var handled = false; - // TODO: These modifiers aren't properly handled, see sway's code - const modifiers = c.wlr_keyboard_get_modifiers(wlr_keyboard); - if (event.state == .WLR_KEY_PRESSED) { - var i: usize = 0; - while (i < translated_keysyms_len) : (i += 1) { - if (self.handleBuiltinMapping(translated_keysyms.?[i])) { - handled = true; - break; - } else if (self.seat.handleMapping(translated_keysyms.?[i], modifiers)) { - handled = true; - break; - } - } - if (!handled) { - i = 0; - while (i < raw_keysyms_len) : (i += 1) { - if (self.handleBuiltinMapping(raw_keysyms.?[i])) { - handled = true; - break; - } else if (self.seat.handleMapping(raw_keysyms.?[i], modifiers)) { - handled = true; - break; - } - } - } - } - - if (!handled) { - // Otherwise, we pass it along to the client. - const wlr_seat = self.seat.wlr_seat; - c.wlr_seat_set_keyboard(wlr_seat, self.wlr_input_device); - c.wlr_seat_keyboard_notify_key( - wlr_seat, - event.time_msec, - event.keycode, - @intCast(u32, @enumToInt(event.state)), - ); - } -} - -fn handleModifiers(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when a modifier key, such as shift or alt, is - // pressed. We simply communicate this to the client. */ - const self = @fieldParentPtr(Self, "listen_modifiers", listener.?); - - // A seat can only have one keyboard, but this is a limitation of the - // Wayland protocol - not wlroots. We assign all connected keyboards to the - // same seat. You can swap out the underlying wlr_keyboard like this and - // wlr_seat handles this transparently. - c.wlr_seat_set_keyboard(self.seat.wlr_seat, self.wlr_input_device); - - // Send modifiers to the client. - c.wlr_seat_keyboard_notify_modifiers( - self.seat.wlr_seat, - &self.wlr_keyboard.modifiers, - ); -} - -/// Handle any builtin, harcoded compsitor mappings such as VT switching. -/// Returns true if the keysym was handled. -fn handleBuiltinMapping(self: Self, keysym: c.xkb_keysym_t) bool { - if (keysym >= c.XKB_KEY_XF86Switch_VT_1 and keysym <= c.XKB_KEY_XF86Switch_VT_12) { - Log.Debug.log("Switch VT keysym received", .{}); - const wlr_backend = self.seat.input_manager.server.wlr_backend; - if (c.river_wlr_backend_is_multi(wlr_backend)) { - if (c.river_wlr_backend_get_session(wlr_backend)) |session| { - const vt = keysym - c.XKB_KEY_XF86Switch_VT_1 + 1; - Log.Debug.log("Switching to VT {}", .{vt}); - _ = c.wlr_session_change_vt(session, vt); - } - } - return true; - } - return false; -} diff --git a/src/LayerSurface.zig b/src/LayerSurface.zig deleted file mode 100644 index 088dd97..0000000 --- a/src/LayerSurface.zig +++ /dev/null @@ -1,196 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const Log = @import("log.zig").Log; -const Output = @import("Output.zig"); -const XdgPopup = @import("XdgPopup.zig"); - -output: *Output, -wlr_layer_surface: *c.wlr_layer_surface_v1, - -box: Box, -layer: c.zwlr_layer_shell_v1_layer, - -// Listeners active the entire lifetime of the layser surface -listen_destroy: c.wl_listener, -listen_map: c.wl_listener, -listen_unmap: c.wl_listener, - -// Listeners only active while the layer surface is mapped -listen_commit: c.wl_listener, -listen_new_popup: c.wl_listener, - -pub fn init( - self: *Self, - output: *Output, - wlr_layer_surface: *c.wlr_layer_surface_v1, -) void { - self.output = output; - self.wlr_layer_surface = wlr_layer_surface; - wlr_layer_surface.data = self; - - self.layer = wlr_layer_surface.client_pending.layer; - - // Temporarily add to the output's list and apply the pending state to allow - // for inital arrangement which sends the first configure. - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - const list = &output.layers[@intCast(usize, @enumToInt(self.layer))]; - const stashed_state = wlr_layer_surface.current; - wlr_layer_surface.current = wlr_layer_surface.client_pending; - list.append(node); - output.arrangeLayers(); - list.remove(node); - wlr_layer_surface.current = stashed_state; - - // Set up listeners that are active for the entire lifetime of the layer surface - self.listen_destroy.notify = handleDestroy; - c.wl_signal_add(&self.wlr_layer_surface.events.destroy, &self.listen_destroy); - - self.listen_map.notify = handleMap; - c.wl_signal_add(&self.wlr_layer_surface.events.map, &self.listen_map); - - self.listen_unmap.notify = handleUnmap; - c.wl_signal_add(&self.wlr_layer_surface.events.unmap, &self.listen_unmap); -} - -fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_destroy", listener.?); - const output = self.output; - - Log.Debug.log("Layer surface '{}' destroyed", .{self.wlr_layer_surface.namespace}); - - // Remove listeners active the entire lifetime of the layer surface - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_map.link); - c.wl_list_remove(&self.listen_unmap.link); - - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - output.root.server.allocator.destroy(node); -} - -fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_map", listener.?); - const wlr_layer_surface = self.wlr_layer_surface; - - Log.Debug.log("Layer surface '{}' mapped.", .{wlr_layer_surface.namespace}); - - // Add listeners that are only active while mapped - self.listen_commit.notify = handleCommit; - c.wl_signal_add(&wlr_layer_surface.surface.*.events.commit, &self.listen_commit); - - self.listen_new_popup.notify = handleNewPopup; - c.wl_signal_add(&wlr_layer_surface.events.new_popup, &self.listen_new_popup); - - c.wlr_surface_send_enter( - wlr_layer_surface.surface, - wlr_layer_surface.output, - ); - - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - self.output.layers[@intCast(usize, @enumToInt(self.layer))].append(node); -} - -fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_unmap", listener.?); - - Log.Debug.log("Layer surface '{}' unmapped.", .{self.wlr_layer_surface.namespace}); - - // This is a bit ugly: we need to use the wlr bool here since surfaces - // may be closed during the inital configure which we preform - // while unmapped. wlroots currently calls unmap unconditionally on close - // even if the surface is not mapped. I sent a patch which was merged, but - // we need to wait for a release to use it. - // - // TODO(wlroots): Remove this check on updating - // https://github.com/swaywm/wlroots/commit/11e94c406bb75c9a8990ce99489798411deb110c - if (self.wlr_layer_surface.mapped) { - // remove listeners only active while the layer surface is mapped - c.wl_list_remove(&self.listen_commit.link); - c.wl_list_remove(&self.listen_new_popup.link); - } - - // Remove from the output's list of layer surfaces - const self_node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - self.output.layers[@intCast(usize, @enumToInt(self.layer))].remove(self_node); - - // If the unmapped surface is focused, clear focus - var it = self.output.root.server.input_manager.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - if (seat.focused_layer) |current_focus| { - if (current_focus == self) { - seat.setFocusRaw(.{ .none = {} }); - } - } - } - - // This gives exclusive focus to a keyboard interactive top or overlay layer - // surface if there is one. - self.output.arrangeLayers(); - - // Ensure that focus is given to the appropriate view if there is no - // other top/overlay layer surface to grab focus. - it = self.output.root.server.input_manager.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - seat.focus(null); - } -} - -fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_commit", listener.?); - const wlr_layer_surface = self.wlr_layer_surface; - - if (self.wlr_layer_surface.output == null) { - Log.Error.log("Layer surface committed with null output", .{}); - return; - } - - // If the layer changed, move the LayerSurface to the proper list - if (self.layer != self.wlr_layer_surface.current.layer) { - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - - const old_layer_idx = @intCast(usize, @enumToInt(self.layer)); - self.output.layers[old_layer_idx].remove(node); - - self.layer = self.wlr_layer_surface.current.layer; - - const new_layer_idx = @intCast(usize, @enumToInt(self.layer)); - self.output.layers[new_layer_idx].append(node); - } - - // TODO: only reconfigure if things haven't changed - // https://github.com/swaywm/wlroots/issues/1079 - self.output.arrangeLayers(); -} - -fn handleNewPopup(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_popup", listener.?); - const wlr_xdg_popup = @ptrCast(*c.wlr_xdg_popup, @alignCast(@alignOf(*c.wlr_xdg_popup), data)); - const allocator = self.output.root.server.allocator; - - // This will free itself on destroy - var xdg_popup = allocator.create(XdgPopup) catch unreachable; - xdg_popup.init(self.output, &self.box, wlr_xdg_popup); -} diff --git a/src/Mapping.zig b/src/Mapping.zig deleted file mode 100644 index 61df624..0000000 --- a/src/Mapping.zig +++ /dev/null @@ -1,46 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -keysym: c.xkb_keysym_t, -modifiers: u32, -command_args: []const []const u8, - -pub fn init( - allocator: *std.mem.Allocator, - keysym: c.xkb_keysym_t, - modifiers: u32, - command_args: []const []const u8, -) !Self { - var owned_args = try allocator.alloc([]u8, command_args.len); - for (command_args) |arg, i| owned_args[i] = try std.mem.dupe(allocator, u8, arg); - return Self{ - .keysym = keysym, - .modifiers = modifiers, - .command_args = owned_args, - }; -} - -pub fn deinit(self: Self, allocator: *std.mem.Allocator) void { - for (self.command_args) |arg| allocator.free(arg); - allocator.free(self.command_args); -} diff --git a/src/Output.zig b/src/Output.zig deleted file mode 100644 index a32c000..0000000 --- a/src/Output.zig +++ /dev/null @@ -1,744 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); -const render = @import("render.zig"); - -const Box = @import("Box.zig"); -const LayerSurface = @import("LayerSurface.zig"); -const Log = @import("log.zig").Log; -const Root = @import("Root.zig"); -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; - -root: *Root, -wlr_output: *c.wlr_output, - -/// All layer surfaces on the output, indexed by the layer enum. -layers: [4]std.TailQueue(LayerSurface), - -/// The area left for views and other layer surfaces after applying the -/// exclusive zones of exclusive layer surfaces. -usable_box: Box, - -/// The top of the stack is the "most important" view. -views: ViewStack(View), - -/// A bit field of focused tags -current_focused_tags: u32, -pending_focused_tags: ?u32, - -/// Number of views in "master" section of the screen. -master_count: u32, - -/// Percentage of the total screen that the master section takes up. -master_factor: f64, - -/// Current layout of the output. -layout: Layout, - -// All listeners for this output, in alphabetical order -listen_destroy: c.wl_listener, -listen_frame: c.wl_listener, -listen_mode: c.wl_listener, - -// All possible layouts. -pub const Layout = enum { - TopMaster, - RightMaster, - BottomMaster, - LeftMaster, - Full, -}; - -const LayoutName = struct { - name: []const u8, - layout: Layout, -}; - -// zig fmt: off -const layout_names = [_]LayoutName { - .{ .name = "TopMaster", .layout = Layout.TopMaster, }, - .{ .name = "RightMaster", .layout = Layout.RightMaster, }, - .{ .name = "BottomMaster", .layout = Layout.BottomMaster, }, - .{ .name = "LeftMaster", .layout = Layout.LeftMaster, }, - .{ .name = "Full", .layout = Layout.Full, }, -}; -// zig fmt: on - -pub fn getLayoutByName(self: Self, name: []const u8) Layout { - for (layout_names) |current| { - if (std.mem.eql(u8, name, current.name)) { - return current.layout; - } - } - Log.Error.log("Layout '{}' does not exist", .{name}); - // In case of error default to LeftMaster - return Layout.LeftMaster; -} - -pub fn init(self: *Self, root: *Root, wlr_output: *c.wlr_output) !void { - // Some backends don't have modes. DRM+KMS does, and we need to set a mode - // before we can use the output. The mode is a tuple of (width, height, - // refresh rate), and each monitor supports only a specific set of modes. We - // just pick the monitor's preferred mode, a more sophisticated compositor - // would let the user configure it. - - // if not empty - if (c.wl_list_empty(&wlr_output.modes) == 0) { - // TODO: handle failure - const mode = c.wlr_output_preferred_mode(wlr_output); - c.wlr_output_set_mode(wlr_output, mode); - c.wlr_output_enable(wlr_output, true); - if (!c.wlr_output_commit(wlr_output)) { - return error.CantCommitWlrOutputMode; - } - } - - self.root = root; - self.wlr_output = wlr_output; - wlr_output.data = self; - - for (self.layers) |*layer| { - layer.* = std.TailQueue(LayerSurface).init(); - } - - self.views.init(); - - self.current_focused_tags = 1 << 0; - self.pending_focused_tags = null; - - self.master_count = 1; - - self.master_factor = 0.6; - - // LeftMaster is the default layout for all outputs - self.layout = Layout.LeftMaster; - - // Set up listeners - self.listen_destroy.notify = handleDestroy; - c.wl_signal_add(&wlr_output.events.destroy, &self.listen_destroy); - - self.listen_frame.notify = handleFrame; - c.wl_signal_add(&wlr_output.events.frame, &self.listen_frame); - - self.listen_mode.notify = handleMode; - c.wl_signal_add(&wlr_output.events.mode, &self.listen_mode); - - if (c.river_wlr_output_is_noop(wlr_output)) { - // A noop output is always 0 x 0 - self.usable_box = .{ - .x = 0, - .y = 0, - .width = 0, - .height = 0, - }; - } else { - // Add the new output to the layout. The add_auto function arranges outputs - // from left-to-right in the order they appear. A more sophisticated - // compositor would let the user configure the arrangement of outputs in the - // layout. This automatically creates an output global on the wl_display. - c.wlr_output_layout_add_auto(root.wlr_output_layout, wlr_output); - - var width: c_int = undefined; - var height: c_int = undefined; - c.wlr_output_effective_resolution(wlr_output, &width, &height); - self.usable_box = .{ - .x = 0, - .y = 0, - .width = @intCast(u32, width), - .height = @intCast(u32, height), - }; - } -} - -pub fn deinit(self: *Self) void { - for (self.layers) |*layer| { - while (layer.pop()) |layer_surface_node| { - self.root.server.allocator.destroy(layer_surface_node); - } - } - - while (self.views.first) |node| { - node.view.deinit(); - self.views.remove(node); - self.root.server.allocator.destroy(node); - } -} - -pub fn getRenderer(self: Self) *c.wlr_renderer { - return c.river_wlr_backend_get_renderer(self.wlr_output.backend); -} - -const MasterPosition = enum { - Top, - Right, - Bottom, - Left, -}; - -/// Default layout of master-stack and slave-stack. -pub fn layoutMasterStack(self: *Self, visible_count: u32, output_tags: u32, position: MasterPosition) void { - const master_count = std.math.min(self.master_count, visible_count); - const slave_count = if (master_count >= visible_count) 0 else visible_count - master_count; - - const border_width = self.root.server.config.border_width; - const view_padding = self.root.server.config.view_padding; - const outer_padding = self.root.server.config.outer_padding; - - const layout_width = @intCast(u32, self.usable_box.width) - outer_padding * 2; - const layout_height = @intCast(u32, self.usable_box.height) - outer_padding * 2; - - // Depending on position of the master area, - // the *_stack_size is either width or height - var master_stack_size: u32 = undefined; - var slave_stack_size: u32 = undefined; - if (master_count > 0 and slave_count > 0) { - // If both master and slave views are present - if (position == MasterPosition.Right or position == MasterPosition.Left) { - master_stack_size = @floatToInt(u32, @round(@intToFloat(f64, layout_width) * self.master_factor)); - slave_stack_size = layout_width - master_stack_size; - } else { - master_stack_size = @floatToInt(u32, @round(@intToFloat(f64, layout_height) * self.master_factor)); - slave_stack_size = layout_height - master_stack_size; - } - } else if (master_count > 0) { - if (position == MasterPosition.Right or position == MasterPosition.Left) { - master_stack_size = layout_width; - } else { - master_stack_size = layout_height; - } - slave_stack_size = 0; - } else { - if (position == MasterPosition.Right or position == MasterPosition.Left) { - slave_stack_size = layout_width; - } else { - slave_stack_size = layout_height; - } - master_stack_size = 0; - } - - var i: u32 = 0; - var it = ViewStack(View).pendingIterator(self.views.first, output_tags); - while (it.next()) |node| { - const view = &node.view; - - if (view.floating) { - continue; - } - - var new_box: Box = undefined; - - // Add the remainder to the first master/slave to ensure every - // pixel of height is used - if (i < master_count) { - if (position == MasterPosition.Top) { // Top master - const master_width = @divTrunc(layout_width, master_count); - const master_width_rem = layout_width % master_count; - new_box = .{ - .x = @intCast(i32, i * master_width + if (i > 0) master_width_rem else 0), - .y = 0, - .width = master_width + if (i == 0) master_width_rem else 0, - .height = master_stack_size, - }; - } else if (position == MasterPosition.Right) { // Right master - const master_height = @divTrunc(layout_height, master_count); - const master_height_rem = layout_height % master_count; - new_box = .{ - .x = @intCast(i32, slave_stack_size), - .y = @intCast(i32, i * master_height + if (i > 0) master_height_rem else 0), - .width = master_stack_size, - .height = master_height + if (i == 0) master_height_rem else 0, - }; - } else if (position == MasterPosition.Bottom) { // Bottom master - const master_width = @divTrunc(layout_width, master_count); - const master_width_rem = layout_width % master_count; - new_box = .{ - .x = @intCast(i32, i * master_width + if (i > 0) master_width_rem else 0), - .y = @intCast(i32, slave_stack_size), - .width = master_width + if (i == 0) master_width_rem else 0, - .height = master_stack_size, - }; - } else { // Left master - const master_height = @divTrunc(layout_height, master_count); - const master_height_rem = layout_height % master_count; - new_box = .{ - .x = 0, - .y = @intCast(i32, i * master_height + if (i > 0) master_height_rem else 0), - .width = master_stack_size, - .height = master_height + if (i == 0) master_height_rem else 0, - }; - } - } else { - if (position == MasterPosition.Top) { // Top master - const slave_width = @divTrunc(layout_width, slave_count); - const slave_width_rem = layout_width % slave_count; - new_box = .{ - .x = @intCast(i32, (i - master_count) * slave_width + if (i > master_count) slave_width_rem else 0), - .y = @intCast(i32, master_stack_size), - .width = slave_width + if (i == master_count) slave_width_rem else 0, - .height = slave_stack_size, - }; - } else if (position == MasterPosition.Right) { // Right master - const slave_height = @divTrunc(layout_height, slave_count); - const slave_height_rem = layout_height % slave_count; - new_box = .{ - .x = 0, - .y = @intCast(i32, (i - master_count) * slave_height + if (i > master_count) slave_height_rem else 0), - .width = slave_stack_size, - .height = slave_height + if (i == master_count) slave_height_rem else 0, - }; - } else if (position == MasterPosition.Bottom) { // Bottom master - const slave_width = @divTrunc(layout_width, slave_count); - const slave_width_rem = layout_width % slave_count; - new_box = .{ - .x = @intCast(i32, (i - master_count) * slave_width + if (i > master_count) slave_width_rem else 0), - .y = 0, - .width = slave_width + if (i == master_count) slave_width_rem else 0, - .height = slave_stack_size, - }; - } else { // Left master - const slave_height = @divTrunc(layout_height, slave_count); - const slave_height_rem = layout_height % slave_count; - new_box = .{ - .x = @intCast(i32, master_stack_size), - .y = @intCast(i32, (i - master_count) * slave_height + if (i > master_count) slave_height_rem else 0), - .width = slave_stack_size, - .height = slave_height + if (i == master_count) slave_height_rem else 0, - }; - } - } - - // Apply offsets from borders and padding - const xy_offset = @intCast(i32, border_width + outer_padding + view_padding); - new_box.x += self.usable_box.x + xy_offset; - new_box.y += self.usable_box.y + xy_offset; - - // Reduce size to allow space for borders/padding - const delta_size = (border_width + view_padding) * 2; - new_box.width -= delta_size; - new_box.height -= delta_size; - - // Set the view's pending box to the new dimensions - view.pending_box = new_box; - - i += 1; - } -} - -/// Wrapper for default layout with master area on the top -pub fn layoutTopMaster(self: *Self, visible_count: u32, output_tags: u32) void { - layoutMasterStack(self, visible_count, output_tags, MasterPosition.Top); -} - -/// Wrapper for default layout with master area on the right -pub fn layoutRightMaster(self: *Self, visible_count: u32, output_tags: u32) void { - layoutMasterStack(self, visible_count, output_tags, MasterPosition.Right); -} - -/// Wrapper for default layout with master area on the bottom -pub fn layoutBottomMaster(self: *Self, visible_count: u32, output_tags: u32) void { - layoutMasterStack(self, visible_count, output_tags, MasterPosition.Bottom); -} - -/// Wrapper for default layout with master area on the left -pub fn layoutLeftMaster(self: *Self, visible_count: u32, output_tags: u32) void { - layoutMasterStack(self, visible_count, output_tags, MasterPosition.Left); -} - -/// A layout in which every window uses the maximum available space. -pub fn layoutFull(self: *Self, visible_count: u32, output_tags: u32) void { - const border_width = self.root.server.config.border_width; - const view_padding = self.root.server.config.view_padding; - const outer_padding = self.root.server.config.outer_padding; - - const layout_width = @intCast(u32, self.usable_box.width) - - (outer_padding * 2) - (border_width * 2) - (view_padding * 2); - const layout_height = @intCast(u32, self.usable_box.height) - - (outer_padding * 2) - (border_width * 2) - (view_padding * 2); - const x_offset = self.usable_box.x + @intCast(i32, outer_padding + border_width + view_padding); - const y_offset = self.usable_box.y + @intCast(i32, outer_padding + border_width + view_padding); - - var i: u32 = 0; - var it = ViewStack(View).pendingIterator(self.views.first, output_tags); - while (it.next()) |node| { - const view = &node.view; - - if (view.floating) { - continue; - } - - var new_box: Box = undefined; - new_box = .{ - .x = x_offset, - .y = y_offset, - .width = layout_width, - .height = layout_height, - }; - - view.pending_box = new_box; - - i += 1; - } -} - -/// Arrange all views on the output for the current layout. Modifies only -/// pending state, the changes are not appplied until a transaction is started -/// and completed. -pub fn arrangeViews(self: *Self) void { - // If the output has a zero dimension, trying to arrange would cause - // underflow and is pointless anyway - if (self.usable_box.width == 0 or self.usable_box.height == 0) { - return; - } - - const output_tags = if (self.pending_focused_tags) |tags| - tags - else - self.current_focused_tags; - - const visible_count = blk: { - var count: u32 = 0; - var it = ViewStack(View).pendingIterator(self.views.first, output_tags); - while (it.next()) |node| { - if (node.view.floating) { - continue; - } - count += 1; - } - break :blk count; - }; - - // A single view should always use the maximum available space. This is - // implemented via the "full" layout to remove the need of every single - // layout to explicitly handle this edge case or the other edge case of - // no visible views. - if (visible_count <= 1) { - layoutFull(self, visible_count, output_tags); - return; - } - - switch (self.layout) { - .Full => layoutFull(self, visible_count, output_tags), - .TopMaster => layoutTopMaster(self, visible_count, output_tags), - .RightMaster => layoutRightMaster(self, visible_count, output_tags), - .BottomMaster => layoutBottomMaster(self, visible_count, output_tags), - .LeftMaster => layoutLeftMaster(self, visible_count, output_tags), - } -} - -/// Arrange all layer surfaces of this output and addjust the usable aread -pub fn arrangeLayers(self: *Self) void { - const full_box = blk: { - var width: c_int = undefined; - var height: c_int = undefined; - c.wlr_output_effective_resolution(self.wlr_output, &width, &height); - break :blk Box{ - .x = 0, - .y = 0, - .width = @intCast(u32, width), - .height = @intCast(u32, height), - }; - }; - - // This box is modified as exclusive zones are applied - var usable_box = full_box; - - const layer_idxs = [_]usize{ - c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, - c.ZWLR_LAYER_SHELL_V1_LAYER_TOP, - c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, - c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, - }; - - // Arrange all layer surfaces with exclusive zones, applying them to the - // usable box along the way. - for (layer_idxs) |layer| { - self.arrangeLayer(self.layers[layer], full_box, &usable_box, true); - } - - // If the the usable_box has changed, we need to rearrange the output - if (!std.meta.eql(self.usable_box, usable_box)) { - self.usable_box = usable_box; - self.root.arrange(); - } - - // Arrange the layers without exclusive zones - for (layer_idxs) |layer| { - self.arrangeLayer(self.layers[layer], full_box, &usable_box, false); - } - - // Find the topmost layer surface in the top or overlay layers which - // requests keyboard interactivity if any. - const topmost_surface = outer: for (layer_idxs[0..2]) |layer| { - // Iterate in reverse order since the last layer is rendered on top - var it = self.layers[layer].last; - while (it) |node| : (it = node.prev) { - const layer_surface = &node.data; - if (layer_surface.wlr_layer_surface.current.keyboard_interactive) { - break :outer layer_surface; - } - } - } else null; - - var it = self.root.server.input_manager.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - - // Only grab focus of seats which have the output focused - if (seat.focused_output != self) { - continue; - } - - if (topmost_surface) |to_focus| { - // If we found a surface that requires focus, grab the focus of all - // seats. - seat.setFocusRaw(.{ .layer = to_focus }); - } else if (seat.focused_layer) |current_focus| { - // If the seat is currently focusing a layer without keyboard - // interactivity, clear the focused layer. - if (!current_focus.wlr_layer_surface.current.keyboard_interactive) { - seat.setFocusRaw(.{ .none = {} }); - seat.focus(null); - } - } - } -} - -/// Arrange the layer surfaces of a given layer -fn arrangeLayer( - self: *Self, - layer: std.TailQueue(LayerSurface), - full_box: Box, - usable_box: *Box, - exclusive: bool, -) void { - var it = layer.first; - while (it) |node| : (it = node.next) { - const layer_surface = &node.data; - const current_state = layer_surface.wlr_layer_surface.current; - - // If the value of exclusive_zone is greater than zero, then it exclusivly - // occupies some area of the screen. - if (exclusive != (current_state.exclusive_zone > 0)) { - continue; - } - - // If the exclusive zone is set to -1, this means the the client would like - // to ignore any exclusive zones and use the full area of the output. - const bounds = if (current_state.exclusive_zone == -1) &full_box else usable_box; - - var new_box: Box = undefined; - - // Horizontal alignment - const anchor_left = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); - const anchor_right = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); - if (current_state.desired_width == 0) { - const anchor_left_right = anchor_left | anchor_right; - if (current_state.anchor & anchor_left_right == anchor_left_right) { - new_box.x = bounds.x + @intCast(i32, current_state.margin.left); - new_box.width = bounds.width - - (current_state.margin.left + current_state.margin.right); - } else { - Log.Error.log( - "Protocol Error: layer surface '{}' requested width 0 without anchoring to opposite edges.", - .{layer_surface.wlr_layer_surface.namespace}, - ); - c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); - continue; - } - } else if (current_state.anchor & anchor_left != 0) { - new_box.x = bounds.x + @intCast(i32, current_state.margin.left); - new_box.width = current_state.desired_width; - } else if (current_state.anchor & anchor_right != 0) { - new_box.x = bounds.x + @intCast(i32, bounds.width - current_state.desired_width - - current_state.margin.right); - new_box.width = current_state.desired_width; - } else { - new_box.x = bounds.x + @intCast(i32, bounds.width / 2 - current_state.desired_width / 2); - new_box.width = current_state.desired_width; - } - - // Vertical alignment - const anchor_top = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP); - const anchor_bottom = @intCast(u32, c.ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM); - if (current_state.desired_height == 0) { - const anchor_top_bottom = anchor_top | anchor_bottom; - if (current_state.anchor & anchor_top_bottom == anchor_top_bottom) { - new_box.y = bounds.y + @intCast(i32, current_state.margin.top); - new_box.height = bounds.height - - (current_state.margin.top + current_state.margin.bottom); - } else { - Log.Error.log( - "Protocol Error: layer surface '{}' requested height 0 without anchoring to opposite edges.", - .{layer_surface.wlr_layer_surface.namespace}, - ); - c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); - continue; - } - } else if (current_state.anchor & anchor_top != 0) { - new_box.y = bounds.y + @intCast(i32, current_state.margin.top); - new_box.height = current_state.desired_height; - } else if (current_state.anchor & anchor_bottom != 0) { - new_box.y = bounds.y + @intCast(i32, bounds.height - current_state.desired_height - - current_state.margin.bottom); - new_box.height = current_state.desired_height; - } else { - new_box.y = bounds.y + @intCast(i32, bounds.height / 2 - current_state.desired_height / 2); - new_box.height = current_state.desired_height; - } - - layer_surface.box = new_box; - - // Apply the exclusive zone to the current bounds - const edges = [4]struct { - anchors: u32, - to_increase: ?*i32, - to_decrease: ?*u32, - margin: u32, - }{ - .{ - .anchors = anchor_left | anchor_right | anchor_top, - .to_increase = &usable_box.y, - .to_decrease = &usable_box.height, - .margin = current_state.margin.top, - }, - .{ - .anchors = anchor_left | anchor_right | anchor_bottom, - .to_increase = null, - .to_decrease = &usable_box.height, - .margin = current_state.margin.bottom, - }, - .{ - .anchors = anchor_left | anchor_top | anchor_bottom, - .to_increase = &usable_box.x, - .to_decrease = &usable_box.width, - .margin = current_state.margin.left, - }, - .{ - .anchors = anchor_right | anchor_top | anchor_bottom, - .to_increase = null, - .to_decrease = &usable_box.width, - .margin = current_state.margin.right, - }, - }; - - for (edges) |edge| { - if (current_state.anchor & edge.anchors == edge.anchors and - current_state.exclusive_zone + @intCast(i32, edge.margin) > 0) - { - const delta = current_state.exclusive_zone + @intCast(i32, edge.margin); - if (edge.to_increase) |value| { - value.* += delta; - } - if (edge.to_decrease) |value| { - value.* -= @intCast(u32, delta); - } - } - } - - // Tell the client to assume the new size - Log.Debug.log("send configure, {} x {}", .{ layer_surface.box.width, layer_surface.box.height }); - c.wlr_layer_surface_v1_configure( - layer_surface.wlr_layer_surface, - layer_surface.box.width, - layer_surface.box.height, - ); - } -} - -/// Called when the output is destroyed. Evacuate all views from the output -/// and then remove it from the list of outputs. -fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_destroy", listener.?); - const root = self.root; - - Log.Debug.log("Output {} destroyed", .{self.wlr_output.name}); - - // Use the first output in the list that is not the one being destroyed. - // If there is no other real output, use the noop output. - var output_it = root.outputs.first; - const fallback_output = while (output_it) |output_node| : (output_it = output_node.next) { - if (&output_node.data != self) { - break &output_node.data; - } - } else &root.noop_output; - - // Move all views from the destroyed output to the fallback one - while (self.views.last) |node| { - const view = &node.view; - view.sendToOutput(fallback_output); - } - - // Close all layer surfaces on the destroyed output - for (self.layers) |*layer, layer_idx| { - while (layer.pop()) |node| { - const layer_surface = &node.data; - // We need to move the closing layer surface to the noop output - // since it may not be immediately destoryed. This just a request - // to close which will trigger unmap and destroy events in - // response, and the LayerSurface needs a valid output to - // handle them. - root.noop_output.layers[layer_idx].prepend(node); - layer_surface.output = &root.noop_output; - c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); - } - } - - // If any seat has the destroyed output focused, focus the fallback one - var seat_it = root.server.input_manager.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - const seat = &seat_node.data; - if (seat.focused_output == self) { - seat.focused_output = fallback_output; - seat.focus(null); - } - } - - // Remove all listeners - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_frame.link); - c.wl_list_remove(&self.listen_mode.link); - - // Clean up the wlr_output - self.wlr_output.data = null; - - // Remove the destroyed output from the list - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - root.outputs.remove(node); - root.server.allocator.destroy(node); - - // Arrange the root in case evacuated views affect the layout - root.arrange(); -} - -fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This function is called every time an output is ready to display a frame, - // generally at the output's refresh rate (e.g. 60Hz). - const self = @fieldParentPtr(Self, "listen_frame", listener.?); - render.renderOutput(self); -} - -fn handleMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_mode", listener.?); - self.arrangeLayers(); - self.root.arrange(); -} diff --git a/src/Root.zig b/src/Root.zig deleted file mode 100644 index f87d7ae..0000000 --- a/src/Root.zig +++ /dev/null @@ -1,247 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); -const build_options = @import("build_options"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Output = @import("Output.zig"); -const Server = @import("Server.zig"); -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); - -/// Responsible for all windowing operations -server: *Server, - -wlr_output_layout: *c.wlr_output_layout, -outputs: std.TailQueue(Output), - -/// This output is used internally when no real outputs are available. -/// It is not advertised to clients. -noop_output: Output, - -/// This list stores all unmanaged Xwayland windows. This needs to be in root -/// since X is like the wild west and who knows where these things will go. -xwayland_unmanaged_views: if (build_options.xwayland) std.TailQueue(XwaylandUnmanaged) else void, - -/// Number of pending configures sent in the current transaction. -/// A value of 0 means there is no current transaction. -pending_configures: u32, - -/// Handles timeout of transactions -transaction_timer: ?*c.wl_event_source, - -pub fn init(self: *Self, server: *Server) !void { - self.server = server; - - // Create an output layout, which a wlroots utility for working with an - // arrangement of screens in a physical layout. - self.wlr_output_layout = c.wlr_output_layout_create() orelse - return error.CantCreateWlrOutputLayout; - errdefer c.wlr_output_layout_destroy(self.wlr_output_layout); - - self.outputs = std.TailQueue(Output).init(); - - const noop_wlr_output = c.river_wlr_noop_add_output(server.noop_backend) orelse - return error.CantAddNoopOutput; - try self.noop_output.init(self, noop_wlr_output); - - if (build_options.xwayland) { - self.xwayland_unmanaged_views = std.TailQueue(XwaylandUnmanaged).init(); - } - - self.pending_configures = 0; - - self.transaction_timer = null; -} - -pub fn deinit(self: *Self) void { - while (self.outputs.pop()) |output_node| { - output_node.data.deinit(); - self.server.allocator.destroy(output_node); - } - c.wlr_output_layout_destroy(self.wlr_output_layout); -} - -pub fn addOutput(self: *Self, wlr_output: *c.wlr_output) void { - // TODO: Handle failure - const node = self.outputs.allocateNode(self.server.allocator) catch unreachable; - node.data.init(self, wlr_output) catch unreachable; - self.outputs.append(node); - - // if we previously had no real outputs, move focus from the noop output - // to the new one. - if (self.outputs.len == 1) { - // TODO: move views from the noop output to the new one and focus(null) - var it = self.server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focused_output = &self.outputs.first.?.data; - } - } -} - -/// Clear the current focus. -pub fn clearFocus(self: *Self) void { - if (self.focused_view) |view| { - _ = c.wlr_xdg_toplevel_set_activated(view.wlr_xdg_surface, false); - } - self.focused_view = null; -} - -/// Arrange all views on all outputs and then start a transaction. -pub fn arrange(self: *Self) void { - var it = self.outputs.first; - while (it) |output_node| : (it = output_node.next) { - output_node.data.arrangeViews(); - } - self.startTransaction(); -} - -/// Initiate an atomic change to the layout. This change will not be -/// applied until all affected clients ack a configure and commit a buffer. -fn startTransaction(self: *Self) void { - // If a new transaction is started while another is in progress, we need - // to reset the pending count to 0 and clear serials from the views - self.pending_configures = 0; - - // Iterate over all views of all outputs - var output_it = self.outputs.first; - while (output_it) |node| : (output_it = node.next) { - const output = &node.data; - var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); - while (view_it.next()) |view_node| { - const view = &view_node.view; - // Clear the serial in case this transaction is interrupting a prior one. - view.pending_serial = null; - - if (view.needsConfigure()) { - view.configure(); - self.pending_configures += 1; - - // We save the current buffer, so we can send an early - // frame done event to give the client a head start on - // redrawing. - view.sendFrameDone(); - } - - // If there is a saved buffer present, then this transaction is interrupting - // a previous transaction and we should keep the old buffer. - if (view.stashed_buffer == null) { - view.stashBuffer(); - } - } - } - - if (self.pending_configures > 0) { - Log.Debug.log( - "Started transaction with {} pending configures.", - .{self.pending_configures}, - ); - - // TODO: log failure to create timer and commit immediately - self.transaction_timer = c.wl_event_loop_add_timer( - self.server.wl_event_loop, - handleTimeout, - self, - ); - - // Set timeout to 200ms - if (c.wl_event_source_timer_update(self.transaction_timer, 200) == -1) { - // TODO: handle failure - } - } else { - self.commitTransaction(); - } -} - -fn handleTimeout(data: ?*c_void) callconv(.C) c_int { - const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); - - Log.Error.log("Transaction timed out. Some imperfect frames may be shown.", .{}); - - self.commitTransaction(); - - return 0; -} - -pub fn notifyConfigured(self: *Self) void { - self.pending_configures -= 1; - if (self.pending_configures == 0) { - // Stop the timer, as we didn't timeout - if (c.wl_event_source_timer_update(self.transaction_timer, 0) == -1) { - // TODO: handle failure - } - self.commitTransaction(); - } -} - -/// Apply the pending state and drop stashed buffers. This means that -/// the next frame drawn will be the post-transaction state of the -/// layout. Should only be called after all clients have configured for -/// the new layout. If called early imperfect frames may be drawn. -fn commitTransaction(self: *Self) void { - // TODO: apply damage properly - - // Ensure this is set to 0 to avoid entering invalid state (e.g. if called due to timeout) - self.pending_configures = 0; - - // Iterate over all views of all outputs - var output_it = self.outputs.first; - while (output_it) |output_node| : (output_it = output_node.next) { - const output = &output_node.data; - - // If there were pending focused tags, make them the current focus - if (output.pending_focused_tags) |tags| { - Log.Debug.log( - "changing current focus: {b:0>10} to {b:0>10}", - .{ output.current_focused_tags, tags }, - ); - output.current_focused_tags = tags; - output.pending_focused_tags = null; - } - - var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); - while (view_it.next()) |view_node| { - const view = &view_node.view; - // Ensure that all pending state is cleared - view.pending_serial = null; - if (view.pending_box) |state| { - view.current_box = state; - view.pending_box = null; - } - - // Apply possible pending tags - if (view.pending_tags) |tags| { - view.current_tags = tags; - view.pending_tags = null; - } - - view.dropStashedBuffer(); - } - } - - // Iterate over all seats and update focus - var it = self.server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focus(null); - } -} diff --git a/src/Seat.zig b/src/Seat.zig deleted file mode 100644 index 82979fc..0000000 --- a/src/Seat.zig +++ /dev/null @@ -1,312 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); -const command = @import("command.zig"); - -const Cursor = @import("Cursor.zig"); -const InputManager = @import("InputManager.zig"); -const Keyboard = @import("Keyboard.zig"); -const LayerSurface = @import("LayerSurface.zig"); -const Output = @import("Output.zig"); -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; - -const FocusTarget = union(enum) { - view: *View, - layer: *LayerSurface, - none: void, -}; - -input_manager: *InputManager, -wlr_seat: *c.wlr_seat, - -/// Multiple mice are handled by the same Cursor -cursor: Cursor, - -/// Mulitple keyboards are handled separately -keyboards: std.TailQueue(Keyboard), - -/// ID of the current keymap mode -mode_id: usize, - -/// Currently focused output, may be the noop output if no -focused_output: *Output, - -/// Currently focused view if any -focused_view: ?*View, - -/// Stack of views in most recently focused order -/// If there is a currently focused view, it is on top. -focus_stack: ViewStack(*View), - -/// Currently focused layer, if any. While this is non-null, no views may -/// recieve focus. -focused_layer: ?*LayerSurface, - -listen_request_set_selection: c.wl_listener, - -pub fn init(self: *Self, input_manager: *InputManager, name: []const u8) !void { - self.input_manager = input_manager; - - // This will be automatically destroyed when the display is destroyed - self.wlr_seat = c.wlr_seat_create(input_manager.server.wl_display, name.ptr) orelse - return error.CantCreateWlrSeat; - - try self.cursor.init(self); - errdefer self.cursor.destroy(); - - self.keyboards = std.TailQueue(Keyboard).init(); - - self.mode_id = 0; - - self.focused_output = &self.input_manager.server.root.noop_output; - - self.focused_view = null; - - self.focus_stack.init(); - - self.focused_layer = null; - - self.listen_request_set_selection.notify = handleRequestSetSelection; - c.wl_signal_add(&self.wlr_seat.events.request_set_selection, &self.listen_request_set_selection); -} - -pub fn deinit(self: *Self) void { - self.cursor.deinit(); - - while (self.keyboards.pop()) |node| { - self.input_manager.server.allocator.destroy(node); - } - - while (self.focus_stack.first) |node| { - self.focus_stack.remove(node); - self.input_manager.server.allocator.destroy(node); - } -} - -/// Set the current focus. If a visible view is passed it will be focused. -/// If null is passed, the first visible view in the focus stack will be focused. -pub fn focus(self: *Self, _view: ?*View) void { - var view = _view; - - // While a layer surface is focused, views may not recieve focus - if (self.focused_layer != null) { - std.debug.assert(self.focused_view == null); - return; - } - - // If view is null or not currently visible - if (if (view) |v| - v.output != self.focused_output or - v.current_tags & self.focused_output.current_focused_tags == 0 - else - true) { - // Set view to the first currently visible view on in the focus stack if any - var it = ViewStack(*View).iterator( - self.focus_stack.first, - self.focused_output.current_focused_tags, - ); - view = while (it.next()) |node| { - if (node.view.output == self.focused_output) { - break node.view; - } - } else null; - } - - if (view) |view_to_focus| { - // Find or allocate a new node in the focus stack for the target view - var it = self.focus_stack.first; - while (it) |node| : (it = node.next) { - // If the view is found, move it to the top of the stack - if (node.view == view_to_focus) { - const new_focus_node = self.focus_stack.remove(node); - self.focus_stack.push(node); - break; - } - } else { - // The view is not in the stack, so allocate a new node and prepend it - const new_focus_node = self.input_manager.server.allocator.create( - ViewStack(*View).Node, - ) catch unreachable; - new_focus_node.view = view_to_focus; - self.focus_stack.push(new_focus_node); - } - - // Focus the target view - self.setFocusRaw(.{ .view = view_to_focus }); - } else { - // Otherwise clear the focus - self.setFocusRaw(.{ .none = {} }); - } -} - -/// Switch focus to the target, handling unfocus and input inhibition -/// properly. This should only be called directly if dealing with layers. -pub fn setFocusRaw(self: *Self, focus_target: FocusTarget) void { - // If the target is already focused, do nothing - if (switch (focus_target) { - .view => |target_view| target_view == self.focused_view, - .layer => |target_layer| target_layer == self.focused_layer, - .none => false, - }) { - return; - } - - // Obtain the target wlr_surface - const target_wlr_surface = switch (focus_target) { - .view => |target_view| target_view.wlr_surface.?, - .layer => |target_layer| target_layer.wlr_layer_surface.surface.?, - .none => null, - }; - - // If input is not allowed on the target surface (e.g. due to an active - // input inhibitor) do not set focus. If there is no target surface we - // still clear the focus. - if (if (target_wlr_surface) |wlr_surface| - self.input_manager.inputAllowed(wlr_surface) - else - true) { - // First clear the current focus - if (self.focused_view) |current_focus| { - std.debug.assert(self.focused_layer == null); - current_focus.setFocused(false); - self.focused_view = null; - } - if (self.focused_layer) |current_focus| { - std.debug.assert(self.focused_view == null); - self.focused_layer = null; - } - c.wlr_seat_keyboard_clear_focus(self.wlr_seat); - - // Set the new focus - switch (focus_target) { - .view => |target_view| { - std.debug.assert(self.focused_output == target_view.output); - target_view.setFocused(true); - self.focused_view = target_view; - }, - .layer => |target_layer| blk: { - std.debug.assert(self.focused_output == target_layer.output); - self.focused_layer = target_layer; - }, - .none => {}, - } - - // Tell wlroots to send the new keyboard focus if we have a target - if (target_wlr_surface) |wlr_surface| { - const keyboard: *c.wlr_keyboard = c.wlr_seat_get_keyboard(self.wlr_seat); - c.wlr_seat_keyboard_notify_enter( - self.wlr_seat, - wlr_surface, - &keyboard.keycodes, - keyboard.num_keycodes, - &keyboard.modifiers, - ); - } - } -} - -/// Handle the unmapping of a view, removing it from the focus stack and -/// setting the focus if needed. -pub fn handleViewUnmap(self: *Self, view: *View) void { - // Remove the node from the focus stack and destroy it. - var it = self.focus_stack.first; - while (it) |node| : (it = node.next) { - if (node.view == view) { - self.focus_stack.remove(node); - self.input_manager.server.allocator.destroy(node); - break; - } - } - - // If the unmapped view is focused, choose a new focus - if (self.focused_view) |current_focus| { - if (current_focus == view) { - self.focus(null); - } - } -} - -/// Handle any user-defined mapping for the passed keysym and modifiers -/// Returns true if the key was handled -pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool { - const modes = &self.input_manager.server.config.modes; - for (modes.items[self.mode_id].items) |mapping| { - if (modifiers == mapping.modifiers and keysym == mapping.keysym) { - // Execute the bound command - const allocator = self.input_manager.server.allocator; - var failure_message: []const u8 = undefined; - command.run(allocator, self, mapping.command_args, &failure_message) catch |err| { - // TODO: log the error - if (err == command.Error.CommandFailed) - allocator.free(failure_message); - }; - return true; - } - } - return false; -} - -/// Add a newly created input device to the seat and update the reported -/// capabilities. -pub fn addDevice(self: *Self, device: *c.wlr_input_device) !void { - switch (device.type) { - .WLR_INPUT_DEVICE_KEYBOARD => self.addKeyboard(device) catch unreachable, - .WLR_INPUT_DEVICE_POINTER => self.addPointer(device), - else => {}, - } - - // We need to let the wlr_seat know what our capabilities are, which is - // communiciated to the client. We always have a cursor, even if - // there are no pointer devices, so we always include that capability. - var caps = @intCast(u32, c.WL_SEAT_CAPABILITY_POINTER); - // if list not empty - if (self.keyboards.len > 0) { - caps |= @intCast(u32, c.WL_SEAT_CAPABILITY_KEYBOARD); - } - c.wlr_seat_set_capabilities(self.wlr_seat, caps); -} - -fn addKeyboard(self: *Self, device: *c.wlr_input_device) !void { - c.wlr_seat_set_keyboard(self.wlr_seat, device); - - const node = try self.keyboards.allocateNode(self.input_manager.server.allocator); - try node.data.init(self, device); - self.keyboards.append(node); -} - -fn addPointer(self: Self, device: *c.struct_wlr_input_device) void { - // We don't do anything special with pointers. All of our pointer handling - // is proxied through wlr_cursor. On another compositor, you might take this - // opportunity to do libinput configuration on the device to set - // acceleration, etc. - c.wlr_cursor_attach_input_device(self.cursor.wlr_cursor, device); -} - -fn handleRequestSetSelection(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_request_set_selection", listener.?); - const event = @ptrCast( - *c.wlr_seat_request_set_selection_event, - @alignCast(@alignOf(*c.wlr_seat_request_set_selection_event), data), - ); - c.wlr_seat_set_selection(self.wlr_seat, event.source, event.serial); -} diff --git a/src/Server.zig b/src/Server.zig deleted file mode 100644 index e46a619..0000000 --- a/src/Server.zig +++ /dev/null @@ -1,279 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const build_options = @import("build_options"); -const std = @import("std"); - -const c = @import("c.zig"); - -const Config = @import("Config.zig"); -const DecorationManager = @import("DecorationManager.zig"); -const InputManager = @import("InputManager.zig"); -const LayerSurface = @import("LayerSurface.zig"); -const Log = @import("log.zig").Log; -const Output = @import("Output.zig"); -const Root = @import("Root.zig"); -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const Control = @import("Control.zig"); -const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); - -allocator: *std.mem.Allocator, - -wl_display: *c.wl_display, -wl_event_loop: *c.wl_event_loop, - -wlr_backend: *c.wlr_backend, -noop_backend: *c.wlr_backend, -listen_new_output: c.wl_listener, - -wlr_xdg_shell: *c.wlr_xdg_shell, -listen_new_xdg_surface: c.wl_listener, - -wlr_layer_shell: *c.wlr_layer_shell_v1, -listen_new_layer_surface: c.wl_listener, - -wlr_xwayland: if (build_options.xwayland) *c.wlr_xwayland else void, -listen_new_xwayland_surface: if (build_options.xwayland) c.wl_listener else void, - -decoration_manager: DecorationManager, -input_manager: InputManager, -root: Root, -config: Config, -control: Control, - -pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { - self.allocator = allocator; - - // The Wayland display is managed by libwayland. It handles accepting - // clients from the Unix socket, managing Wayland globals, and so on. - self.wl_display = c.wl_display_create() orelse - return error.CantCreateWlDisplay; - errdefer c.wl_display_destroy(self.wl_display); - - // Should never return null if the display was created successfully - self.wl_event_loop = c.wl_display_get_event_loop(self.wl_display) orelse - return error.CantGetEventLoop; - - // The wlr_backend abstracts the input/output hardware. Autocreate chooses - // the best option based on the environment, for example DRM when run from - // a tty or wayland if WAYLAND_DISPLAY is set. This frees itself when the - // wl_display is destroyed. - self.wlr_backend = c.river_wlr_backend_autocreate(self.wl_display) orelse - return error.CantCreateWlrBackend; - - // This backend is used to create a noop output for use when no actual - // outputs are available. This frees itself when the wl_display is destroyed. - self.noop_backend = c.river_wlr_noop_backend_create(self.wl_display) orelse - return error.CantCreateNoopBackend; - - // If we don't provide a renderer, autocreate makes a GLES2 renderer for us. - // The renderer is responsible for defining the various pixel formats it - // supports for shared memory, this configures that for clients. - const wlr_renderer = c.river_wlr_backend_get_renderer(self.wlr_backend) orelse - return error.CantGetWlrRenderer; - // TODO: Handle failure after https://github.com/swaywm/wlroots/pull/2080 - c.wlr_renderer_init_wl_display(wlr_renderer, self.wl_display); // orelse - // return error.CantInitWlDisplay; - self.listen_new_output.notify = handleNewOutput; - c.wl_signal_add(&self.wlr_backend.events.new_output, &self.listen_new_output); - - const wlr_compositor = c.wlr_compositor_create(self.wl_display, wlr_renderer) orelse - return error.CantCreateWlrCompositor; - - // Set up xdg shell - self.wlr_xdg_shell = c.wlr_xdg_shell_create(self.wl_display) orelse - return error.CantCreateWlrXdgShell; - self.listen_new_xdg_surface.notify = handleNewXdgSurface; - c.wl_signal_add(&self.wlr_xdg_shell.events.new_surface, &self.listen_new_xdg_surface); - - // Set up layer shell - self.wlr_layer_shell = c.wlr_layer_shell_v1_create(self.wl_display) orelse - return error.CantCreateWlrLayerShell; - self.listen_new_layer_surface.notify = handleNewLayerSurface; - c.wl_signal_add(&self.wlr_layer_shell.events.new_surface, &self.listen_new_layer_surface); - - // Set up xwayland if built with support - if (build_options.xwayland) { - self.wlr_xwayland = c.wlr_xwayland_create(self.wl_display, wlr_compositor, false) orelse - return error.CantCreateWlrXwayland; - self.listen_new_xwayland_surface.notify = handleNewXwaylandSurface; - c.wl_signal_add(&self.wlr_xwayland.events.new_surface, &self.listen_new_xwayland_surface); - } - - try self.config.init(self.allocator); - try self.decoration_manager.init(self); - try self.root.init(self); - // Must be called after root is initialized - try self.input_manager.init(self); - try self.control.init(self); - - // These all free themselves when the wl_display is destroyed - _ = c.wlr_data_device_manager_create(self.wl_display) orelse - return error.CantCreateWlrDataDeviceManager; - _ = c.wlr_screencopy_manager_v1_create(self.wl_display) orelse - return error.CantCreateWlrScreencopyManager; - _ = c.wlr_xdg_output_manager_v1_create(self.wl_display, self.root.wlr_output_layout) orelse - return error.CantCreateWlrOutputManager; -} - -/// Free allocated memory and clean up -pub fn deinit(self: *Self) void { - // Note: order is important here - if (build_options.xwayland) { - c.wlr_xwayland_destroy(self.wlr_xwayland); - } - c.wl_display_destroy_clients(self.wl_display); - c.wl_display_destroy(self.wl_display); - self.input_manager.deinit(); - self.root.deinit(); - self.config.deinit(self.allocator); -} - -/// Create the socket, set WAYLAND_DISPLAY, and start the backend -pub fn start(self: Self) !void { - // Add a Unix socket to the Wayland display. - const socket = c.wl_display_add_socket_auto(self.wl_display) orelse - return error.CantAddSocket; - - // Start the backend. This will enumerate outputs and inputs, become the DRM - // master, etc - if (!c.river_wlr_backend_start(self.wlr_backend)) { - return error.CantStartBackend; - } - - // Set the WAYLAND_DISPLAY environment variable to our socket - if (c.setenv("WAYLAND_DISPLAY", socket, 1) == -1) { - return error.CantSetEnv; - } - - if (build_options.xwayland) { - if (c.setenv("DISPLAY", &self.wlr_xwayland.display_name, 1) == -1) { - return error.CantSetEnv; - } - } -} - -/// Enter the wayland event loop and block until the compositor is exited -pub fn run(self: Self) void { - c.wl_display_run(self.wl_display); -} - -fn handleNewOutput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_output", listener.?); - const wlr_output = @ptrCast(*c.wlr_output, @alignCast(@alignOf(*c.wlr_output), data)); - Log.Debug.log("New output {}", .{wlr_output.name}); - self.root.addOutput(wlr_output); -} - -fn handleNewXdgSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when wlr_xdg_shell receives a new xdg surface from a - // client, either a toplevel (application window) or popup. - const self = @fieldParentPtr(Self, "listen_new_xdg_surface", listener.?); - const wlr_xdg_surface = @ptrCast(*c.wlr_xdg_surface, @alignCast(@alignOf(*c.wlr_xdg_surface), data)); - - if (wlr_xdg_surface.role == .WLR_XDG_SURFACE_ROLE_POPUP) { - Log.Debug.log("New xdg_popup", .{}); - return; - } - - Log.Debug.log("New xdg_toplevel", .{}); - - // The View will add itself to the output's view stack on map - const output = self.input_manager.default_seat.focused_output; - const node = self.allocator.create(ViewStack(View).Node) catch unreachable; - node.view.init(output, output.current_focused_tags, wlr_xdg_surface); -} - -/// This event is raised when the layer_shell recieves a new surface from a client. -fn handleNewLayerSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_layer_surface", listener.?); - const wlr_layer_surface = @ptrCast( - *c.wlr_layer_surface_v1, - @alignCast(@alignOf(*c.wlr_layer_surface_v1), data), - ); - - Log.Debug.log( - "New layer surface: namespace {}, layer {}, anchor {}, size {}x{}, margin ({},{},{},{}), exclusive_zone {}", - .{ - wlr_layer_surface.namespace, - wlr_layer_surface.client_pending.layer, - wlr_layer_surface.client_pending.anchor, - wlr_layer_surface.client_pending.desired_width, - wlr_layer_surface.client_pending.desired_height, - wlr_layer_surface.client_pending.margin.top, - wlr_layer_surface.client_pending.margin.right, - wlr_layer_surface.client_pending.margin.bottom, - wlr_layer_surface.client_pending.margin.left, - wlr_layer_surface.client_pending.exclusive_zone, - }, - ); - - // If the new layer surface does not have an output assigned to it, use the - // first output or close the surface if none are available. - if (wlr_layer_surface.output == null) { - if (self.root.outputs.first) |node| { - const output = &node.data; - Log.Debug.log( - "New layer surface had null output, assigning it to output {}", - .{output.wlr_output.name}, - ); - wlr_layer_surface.output = output.wlr_output; - } else { - Log.Error.log( - "No output available for layer surface '{}'", - .{wlr_layer_surface.namespace}, - ); - c.wlr_layer_surface_v1_close(wlr_layer_surface); - return; - } - } - - // The layer surface will add itself to the proper list of the output on map - const output = @ptrCast(*Output, @alignCast(@alignOf(*Output), wlr_layer_surface.output.*.data)); - const node = self.allocator.create(std.TailQueue(LayerSurface).Node) catch unreachable; - node.data.init(output, wlr_layer_surface); -} - -fn handleNewXwaylandSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_xwayland_surface", listener.?); - const wlr_xwayland_surface = @ptrCast( - *c.wlr_xwayland_surface, - @alignCast(@alignOf(*c.wlr_xwayland_surface), data), - ); - - if (wlr_xwayland_surface.override_redirect) { - Log.Debug.log("New unmanaged xwayland surface", .{}); - // The unmanged surface will add itself to the list of unmanaged views - // in Root when it is mapped. - const node = self.allocator.create(std.TailQueue(XwaylandUnmanaged).Node) catch unreachable; - node.data.init(&self.root, wlr_xwayland_surface); - return; - } - - Log.Debug.log( - "New xwayland surface: title '{}', class '{}'", - .{ wlr_xwayland_surface.title, wlr_xwayland_surface.class }, - ); - - // The View will add itself to the output's view stack on map - const output = self.input_manager.default_seat.focused_output; - const node = self.allocator.create(ViewStack(View).Node) catch unreachable; - node.view.init(output, output.current_focused_tags, wlr_xwayland_surface); -} diff --git a/src/View.zig b/src/View.zig deleted file mode 100644 index 8ab32cc..0000000 --- a/src/View.zig +++ /dev/null @@ -1,271 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const build_options = @import("build_options"); -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const Log = @import("log.zig").Log; -const Output = @import("Output.zig"); -const Root = @import("Root.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const XdgToplevel = @import("XdgToplevel.zig"); -const XwaylandView = if (build_options.xwayland) - @import("XwaylandView.zig") -else - @import("VoidView.zig"); - -const Impl = union(enum) { - xdg_toplevel: XdgToplevel, - xwayland_view: XwaylandView, -}; - -/// The implementation of this view -impl: Impl, - -/// The output this view is currently associated with -output: *Output, - -/// This is non-null exactly when the view is mapped -wlr_surface: ?*c.wlr_surface, - -/// If the view is floating or not -floating: bool, - -/// True if the view is currentlt focused by at lease one seat -focused: bool, - -/// The current output-relative coordinates and dimensions of the view -current_box: Box, -pending_box: ?Box, - -/// The dimensions the view would have taken if we didn't force it to tile -natural_width: u32, -natural_height: u32, - -current_tags: u32, -pending_tags: ?u32, - -pending_serial: ?u32, - -// This is what we render while a transaction is in progress -stashed_buffer: ?*c.wlr_buffer, - -pub fn init( - self: *Self, - output: *Output, - tags: u32, - surface: var, -) void { - self.output = output; - - self.wlr_surface = null; - - self.focused = false; - - self.current_box = Box{ - .x = 0, - .y = 0, - .height = 0, - .width = 0, - }; - self.pending_box = null; - - self.current_tags = tags; - self.pending_tags = null; - - self.pending_serial = null; - - self.stashed_buffer = null; - - if (@TypeOf(surface) == *c.wlr_xdg_surface) { - self.impl = .{ .xdg_toplevel = undefined }; - self.impl.xdg_toplevel.init(self, surface); - } else if (build_options.xwayland and @TypeOf(surface) == *c.wlr_xwayland_surface) { - self.impl = .{ .xwayland_view = undefined }; - self.impl.xwayland_view.init(self, surface); - } else unreachable; -} - -pub fn deinit(self: Self) void { - if (self.stashed_buffer) |buffer| { - c.wlr_buffer_unref(buffer); - } -} - -pub fn needsConfigure(self: Self) bool { - if (self.pending_box) |pending_box| { - return pending_box.width != self.current_box.width or - pending_box.height != self.current_box.height; - } else { - return false; - } -} - -pub fn configure(self: Self) void { - if (self.pending_box) |pending_box| { - switch (self.impl) { - .xdg_toplevel => |xdg_toplevel| xdg_toplevel.configure(pending_box), - .xwayland_view => |xwayland_view| xwayland_view.configure(pending_box), - } - } else { - Log.Error.log("Configure called on a View with no pending box", .{}); - } -} - -pub fn sendFrameDone(self: Self) void { - var now: c.timespec = undefined; - _ = c.clock_gettime(c.CLOCK_MONOTONIC, &now); - c.wlr_surface_send_frame_done(self.wlr_surface.?, &now); -} - -pub fn dropStashedBuffer(self: *Self) void { - // TODO: log debug error - if (self.stashed_buffer) |buffer| { - c.wlr_buffer_unref(buffer); - self.stashed_buffer = null; - } -} - -pub fn stashBuffer(self: *Self) void { - // TODO: log debug error if there is already a saved buffer - if (self.wlr_surface) |wlr_surface| { - if (c.wlr_surface_has_buffer(wlr_surface)) { - _ = c.wlr_buffer_ref(wlr_surface.buffer); - self.stashed_buffer = wlr_surface.buffer; - } - } -} - -/// Set the focued bool and the active state of the view if it is a toplevel -pub fn setFocused(self: *Self, focused: bool) void { - self.focused = focused; - switch (self.impl) { - .xdg_toplevel => |xdg_toplevel| xdg_toplevel.setActivated(focused), - .xwayland_view => |xwayland_view| xwayland_view.setActivated(focused), - } -} - -/// If true is passsed, make the view float. If false, return it to the tiled -/// layout. -pub fn setFloating(self: *Self, float: bool) void { - if (float and !self.floating) { - self.floating = true; - self.pending_box = Box{ - .x = std.math.max(0, @divTrunc(@intCast(i32, self.output.usable_box.width) - - @intCast(i32, self.natural_width), 2)), - .y = std.math.max(0, @divTrunc(@intCast(i32, self.output.usable_box.height) - - @intCast(i32, self.natural_height), 2)), - .width = self.natural_width, - .height = self.natural_height, - }; - } else if (!float and self.floating) { - self.floating = false; - } -} - -/// Move a view from one output to another, sending the required enter/leave -/// events. -pub fn sendToOutput(self: *Self, destination_output: *Output) void { - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); - - self.output.views.remove(node); - destination_output.views.push(node); - - c.wlr_surface_send_leave(self.wlr_surface, self.output.wlr_output); - c.wlr_surface_send_enter(self.wlr_surface, destination_output.wlr_output); - - self.output = destination_output; -} - -pub fn close(self: Self) void { - switch (self.impl) { - .xdg_toplevel => |xdg_toplevel| xdg_toplevel.close(), - .xwayland_view => |xwayland_view| xwayland_view.close(), - } -} - -pub fn forEachSurface( - self: Self, - iterator: c.wlr_surface_iterator_func_t, - user_data: ?*c_void, -) void { - switch (self.impl) { - .xdg_toplevel => |xdg_toplevel| xdg_toplevel.forEachSurface(iterator, user_data), - .xwayland_view => |xwayland_view| xwayland_view.forEachSurface(iterator, user_data), - } -} - -/// Return the surface at output coordinates ox, oy and set sx, sy to the -/// corresponding surface-relative coordinates, if there is a surface. -pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { - return switch (self.impl) { - .xdg_toplevel => |xdg_toplevel| xdg_toplevel.surfaceAt(ox, oy, sx, sy), - .xwayland_view => |xwayland_view| xwayland_view.surfaceAt(ox, oy, sx, sy), - }; -} - -/// Called by the impl when the surface is ready to be displayed -pub fn map(self: *Self) void { - const root = self.output.root; - - // Add the view to the stack of its output - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); - self.output.views.push(node); - - // Focus the newly mapped view. Note: if a seat is focusing a different output - // it will continue to do so. - var it = root.server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focus(self); - } - - c.wlr_surface_send_enter(self.wlr_surface.?, self.output.wlr_output); - - root.arrange(); -} - -/// Called by the impl when the surface will no longer be displayed -pub fn unmap(self: *Self) void { - const root = self.output.root; - - self.wlr_surface = null; - - // Inform all seats that the view has been unmapped so they can handle focus - var it = root.server.input_manager.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - seat.handleViewUnmap(self); - } - - // Remove the view from its output's stack - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); - self.output.views.remove(node); - - root.arrange(); -} - -/// Destory the view and free the ViewStack node holding it. -pub fn destroy(self: *const Self) void { - self.deinit(); - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); - self.output.root.server.allocator.destroy(node); -} diff --git a/src/VoidView.zig b/src/VoidView.zig deleted file mode 100644 index 72c57a7..0000000 --- a/src/VoidView.zig +++ /dev/null @@ -1,48 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); - -pub fn configure(self: Self, pending_box: Box) void { - unreachable; -} - -pub fn setActivated(self: Self, activated: bool) void { - unreachable; -} - -pub fn close(self: Self) void { - unreachable; -} - -pub fn forEachSurface( - self: Self, - iterator: c.wlr_surface_iterator_func_t, - user_data: ?*c_void, -) void { - unreachable; -} - -pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { - unreachable; -} diff --git a/src/XdgPopup.zig b/src/XdgPopup.zig deleted file mode 100644 index b4ee56c..0000000 --- a/src/XdgPopup.zig +++ /dev/null @@ -1,82 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const Output = @import("Output.zig"); - -/// The output this popup is displayed on. -output: *Output, - -/// Box of the parent of this popup tree. Needed to unconstrain child popups. -parent_box: *const Box, - -/// The corresponding wlroots object -wlr_xdg_popup: *c.wlr_xdg_popup, - -listen_destroy: c.wl_listener, -listen_new_popup: c.wl_listener, - -pub fn init( - self: *Self, - output: *Output, - parent_box: *const Box, - wlr_xdg_popup: *c.wlr_xdg_popup, -) void { - self.output = output; - self.parent_box = parent_box; - self.wlr_xdg_popup = wlr_xdg_popup; - - // The output box relative to the parent of the popup - var box = c.wlr_output_layout_get_box(output.root.wlr_output_layout, output.wlr_output).*; - box.x -= parent_box.x; - box.y -= parent_box.y; - c.wlr_xdg_popup_unconstrain_from_box(wlr_xdg_popup, &box); - - // Setup listeners - self.listen_destroy.notify = handleDestroy; - c.wl_signal_add(&wlr_xdg_popup.base.*.events.destroy, &self.listen_destroy); - - self.listen_new_popup.notify = handleNewPopup; - c.wl_signal_add(&wlr_xdg_popup.base.*.events.new_popup, &self.listen_new_popup); -} - -fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_destroy", listener.?); - const allocator = self.output.root.server.allocator; - - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_new_popup.link); - - allocator.destroy(self); -} - -/// Called when a new xdg popup is requested by the client -fn handleNewPopup(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_popup", listener.?); - const wlr_xdg_popup = @ptrCast(*c.wlr_xdg_popup, @alignCast(@alignOf(*c.wlr_xdg_popup), data)); - const allocator = self.output.root.server.allocator; - - // This will free itself on destroy - var xdg_popup = allocator.create(Self) catch unreachable; - xdg_popup.init(self.output, self.parent_box, wlr_xdg_popup); -} diff --git a/src/XdgToplevel.zig b/src/XdgToplevel.zig deleted file mode 100644 index b3806db..0000000 --- a/src/XdgToplevel.zig +++ /dev/null @@ -1,201 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const Log = @import("log.zig").Log; -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const XdgPopup = @import("XdgPopup.zig"); - -/// The view this xdg toplevel implements -view: *View, - -/// The corresponding wlroots object -wlr_xdg_surface: *c.wlr_xdg_surface, - -// Listeners that are always active over the view's lifetime -listen_destroy: c.wl_listener, -listen_map: c.wl_listener, -listen_unmap: c.wl_listener, - -// Listeners that are only active while the view is mapped -listen_commit: c.wl_listener, -listen_new_popup: c.wl_listener, - -pub fn init(self: *Self, view: *View, wlr_xdg_surface: *c.wlr_xdg_surface) void { - self.view = view; - self.wlr_xdg_surface = wlr_xdg_surface; - wlr_xdg_surface.data = self; - - // Inform the xdg toplevel that it is tiled. - // For example this prevents firefox from drawing shadows around itself - _ = c.wlr_xdg_toplevel_set_tiled(self.wlr_xdg_surface, c.WLR_EDGE_LEFT | - c.WLR_EDGE_RIGHT | c.WLR_EDGE_TOP | c.WLR_EDGE_BOTTOM); - - // Add listeners that are active over the view's entire lifetime - self.listen_destroy.notify = handleDestroy; - c.wl_signal_add(&self.wlr_xdg_surface.events.destroy, &self.listen_destroy); - - self.listen_map.notify = handleMap; - c.wl_signal_add(&self.wlr_xdg_surface.events.map, &self.listen_map); - - self.listen_unmap.notify = handleUnmap; - c.wl_signal_add(&self.wlr_xdg_surface.events.unmap, &self.listen_unmap); -} - -pub fn configure(self: Self, pending_box: Box) void { - self.view.pending_serial = c.wlr_xdg_toplevel_set_size( - self.wlr_xdg_surface, - pending_box.width, - pending_box.height, - ); -} - -pub fn setActivated(self: Self, activated: bool) void { - _ = c.wlr_xdg_toplevel_set_activated(self.wlr_xdg_surface, activated); -} - -/// Close the view. This will lead to the unmap and destroy events being sent -pub fn close(self: Self) void { - c.wlr_xdg_toplevel_send_close(self.wlr_xdg_surface); -} - -pub fn forEachSurface( - self: Self, - iterator: c.wlr_surface_iterator_func_t, - user_data: ?*c_void, -) void { - c.wlr_xdg_surface_for_each_surface(self.wlr_xdg_surface, iterator, user_data); -} - -/// Return the surface at output coordinates ox, oy and set sx, sy to the -/// corresponding surface-relative coordinates, if there is a surface. -pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { - return c.wlr_xdg_surface_surface_at( - self.wlr_xdg_surface, - ox - @intToFloat(f64, self.view.current_box.x), - oy - @intToFloat(f64, self.view.current_box.y), - sx, - sy, - ); -} - -/// Called when the xdg surface is destroyed -fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_destroy", listener.?); - const output = self.view.output; - - // Remove listeners that are active for the entire lifetime of the view - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_map.link); - c.wl_list_remove(&self.listen_unmap.link); - - self.view.destroy(); -} - -/// Called when the xdg surface is mapped, or ready to display on-screen. -fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_map", listener.?); - const view = self.view; - const root = view.output.root; - - // Add listeners that are only active while mapped - self.listen_commit.notify = handleCommit; - c.wl_signal_add(&self.wlr_xdg_surface.surface.*.events.commit, &self.listen_commit); - - self.listen_new_popup.notify = handleNewPopup; - c.wl_signal_add(&self.wlr_xdg_surface.events.new_popup, &self.listen_new_popup); - - view.wlr_surface = self.wlr_xdg_surface.surface; - view.floating = false; - - view.natural_width = @intCast(u32, self.wlr_xdg_surface.geometry.width); - view.natural_height = @intCast(u32, self.wlr_xdg_surface.geometry.height); - - if (view.natural_width == 0 and view.natural_height == 0) { - view.natural_width = @intCast(u32, self.wlr_xdg_surface.surface.*.current.width); - view.natural_height = @intCast(u32, self.wlr_xdg_surface.surface.*.current.height); - } - - const wlr_xdg_toplevel: *c.wlr_xdg_toplevel = @field( - self.wlr_xdg_surface, - c.wlr_xdg_surface_union, - ).toplevel; - const state = &wlr_xdg_toplevel.current; - const app_id: [*:0]const u8 = if (wlr_xdg_toplevel.app_id) |id| id else "NULL"; - - Log.Debug.log("View with app_id '{}' mapped", .{app_id}); - - for (root.server.config.float_filter.items) |filter_app_id| { - // Make views with app_ids listed in the float filter float - if (std.mem.eql(u8, std.mem.span(app_id), std.mem.span(filter_app_id))) { - view.setFloating(true); - break; - } - } else if ((wlr_xdg_toplevel.parent != null) or - (state.min_width != 0 and state.min_height != 0 and - (state.min_width == state.max_width or state.min_height == state.max_height))) - { - // If the toplevel has a parent or is of fixed size make it float - view.setFloating(true); - } - - view.map(); -} - -/// Called when the surface is unmapped and will no longer be displayed. -fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_unmap", listener.?); - const root = self.view.output.root; - - self.view.unmap(); - - // Remove listeners that are only active while mapped - c.wl_list_remove(&self.listen_commit.link); - c.wl_list_remove(&self.listen_new_popup.link); -} - -/// Called when the surface is comitted -/// TODO: check for unexpected change in size and react as needed -fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_commit", listener.?); - const view = self.view; - - if (view.pending_serial) |s| { - if (s == self.wlr_xdg_surface.configure_serial) { - view.output.root.notifyConfigured(); - view.pending_serial = null; - } - } -} - -/// Called when a new xdg popup is requested by the client -fn handleNewPopup(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_popup", listener.?); - const wlr_xdg_popup = @ptrCast(*c.wlr_xdg_popup, @alignCast(@alignOf(*c.wlr_xdg_popup), data)); - const allocator = self.view.output.root.server.allocator; - - // This will free itself on destroy - var xdg_popup = allocator.create(XdgPopup) catch unreachable; - xdg_popup.init(self.view.output, &self.view.current_box, wlr_xdg_popup); -} diff --git a/src/XwaylandUnmanaged.zig b/src/XwaylandUnmanaged.zig deleted file mode 100644 index fe33e73..0000000 --- a/src/XwaylandUnmanaged.zig +++ /dev/null @@ -1,136 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const Log = @import("log.zig").Log; -const Root = @import("Root.zig"); - -root: *Root, - -/// The corresponding wlroots object -wlr_xwayland_surface: *c.wlr_xwayland_surface, - -// Listeners that are always active over the view's lifetime -liseten_request_configure: c.wl_listener, -listen_destroy: c.wl_listener, -listen_map: c.wl_listener, -listen_unmap: c.wl_listener, - -// Listeners that are only active while the view is mapped -listen_commit: c.wl_listener, - -pub fn init(self: *Self, root: *Root, wlr_xwayland_surface: *c.wlr_xwayland_surface) void { - self.root = root; - self.wlr_xwayland_surface = wlr_xwayland_surface; - - // Add listeners that are active over the view's entire lifetime - self.liseten_request_configure.notify = handleRequestConfigure; - c.wl_signal_add(&wlr_xwayland_surface.events.request_configure, &self.liseten_request_configure); - - self.listen_destroy.notify = handleDestroy; - c.wl_signal_add(&wlr_xwayland_surface.events.destroy, &self.listen_destroy); - - self.listen_map.notify = handleMap; - c.wl_signal_add(&wlr_xwayland_surface.events.map, &self.listen_map); - - self.listen_unmap.notify = handleUnmap; - c.wl_signal_add(&wlr_xwayland_surface.events.unmap, &self.listen_unmap); -} - -/// Return the surface at output coordinates ox, oy and set sx, sy to the -/// corresponding surface-relative coordinates, if there is a surface. -pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { - return c.wlr_surface_surface_at( - self.wlr_xwayland_surface.surface, - ox - @intToFloat(f64, self.view.current_box.x), - oy - @intToFloat(f64, self.view.current_box.y), - sx, - sy, - ); -} - -fn handleRequestConfigure(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "liseten_request_configure", listener.?); - const wlr_xwayland_surface_configure_event = @ptrCast( - *c.wlr_xwayland_surface_configure_event, - @alignCast(@alignOf(*c.wlr_xwayland_surface_configure_event), data), - ); - c.wlr_xwayland_surface_configure( - self.wlr_xwayland_surface, - wlr_xwayland_surface_configure_event.x, - wlr_xwayland_surface_configure_event.y, - wlr_xwayland_surface_configure_event.width, - wlr_xwayland_surface_configure_event.height, - ); -} - -/// Called when the xwayland surface is destroyed -fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_destroy", listener.?); - - // Remove listeners that are active for the entire lifetime of the view - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_map.link); - c.wl_list_remove(&self.listen_unmap.link); - - // Deallocate the node - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - self.root.server.allocator.destroy(node); -} - -/// Called when the xwayland surface is mapped, or ready to display on-screen. -fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_map", listener.?); - const root = self.root; - - // Add self to the list of unmanaged views in the root - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - root.xwayland_unmanaged_views.prepend(node); - - // Add listeners that are only active while mapped - self.listen_commit.notify = handleCommit; - c.wl_signal_add(&self.wlr_xwayland_surface.surface.*.events.commit, &self.listen_commit); - - // TODO: handle keyboard focus - // if (wlr_xwayland_or_surface_wants_focus(self.wlr_xwayland_surface)) { ... -} - -/// Called when the surface is unmapped and will no longer be displayed. -fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_unmap", listener.?); - - // Remove self from the list of unmanged views in the root - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - self.root.xwayland_unmanaged_views.remove(node); - - // Remove listeners that are only active while mapped - c.wl_list_remove(&self.listen_commit.link); - - // TODO: return focus -} - -/// Called when the surface is comitted -fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_commit", listener.?); - // TODO: check if the surface has moved for damage tracking -} diff --git a/src/XwaylandView.zig b/src/XwaylandView.zig deleted file mode 100644 index dbd35e4..0000000 --- a/src/XwaylandView.zig +++ /dev/null @@ -1,159 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const Log = @import("log.zig").Log; -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const XdgPopup = @import("XdgPopup.zig"); - -/// The view this xwayland view implements -view: *View, - -/// The corresponding wlroots object -wlr_xwayland_surface: *c.wlr_xwayland_surface, - -// Listeners that are always active over the view's lifetime -listen_destroy: c.wl_listener, -listen_map: c.wl_listener, -listen_unmap: c.wl_listener, - -// Listeners that are only active while the view is mapped -listen_commit: c.wl_listener, - -pub fn init(self: *Self, view: *View, wlr_xwayland_surface: *c.wlr_xwayland_surface) void { - self.view = view; - self.wlr_xwayland_surface = wlr_xwayland_surface; - wlr_xwayland_surface.data = self; - - // Add listeners that are active over the view's entire lifetime - self.listen_destroy.notify = handleDestroy; - c.wl_signal_add(&self.wlr_xwayland_surface.events.destroy, &self.listen_destroy); - - self.listen_map.notify = handleMap; - c.wl_signal_add(&self.wlr_xwayland_surface.events.map, &self.listen_map); - - self.listen_unmap.notify = handleUnmap; - c.wl_signal_add(&self.wlr_xwayland_surface.events.unmap, &self.listen_unmap); -} - -/// Tell the client to take a new size -pub fn configure(self: Self, pending_box: Box) void { - c.wlr_xwayland_surface_configure( - self.wlr_xwayland_surface, - @intCast(i16, pending_box.x), - @intCast(i16, pending_box.y), - @intCast(u16, pending_box.width), - @intCast(u16, pending_box.height), - ); - // Xwayland surfaces don't use serials, so we will just assume they have - // configured the next time they commit. Set pending serial to a dummy - // value to indicate that a transaction has started. Note: we can't just - // call notifyConfigured() here as the transaction has not yet been fully - // initiated. - self.view.pending_serial = 0x66666666; -} - -/// Inform the xwayland surface that it has gained focus -pub fn setActivated(self: Self, activated: bool) void { - c.wlr_xwayland_surface_activate(self.wlr_xwayland_surface, activated); -} - -/// Close the view. This will lead to the unmap and destroy events being sent -pub fn close(self: Self) void { - c.wlr_xwayland_surface_close(self.wlr_xwayland_surface); -} - -/// Iterate over all surfaces of the xwayland view. -pub fn forEachSurface( - self: Self, - iterator: c.wlr_surface_iterator_func_t, - user_data: ?*c_void, -) void { - c.wlr_surface_for_each_surface(self.wlr_xwayland_surface.surface, iterator, user_data); -} - -/// Return the surface at output coordinates ox, oy and set sx, sy to the -/// corresponding surface-relative coordinates, if there is a surface. -pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { - return c.wlr_surface_surface_at( - self.wlr_xwayland_surface.surface, - ox - @intToFloat(f64, self.view.current_box.x), - oy - @intToFloat(f64, self.view.current_box.y), - sx, - sy, - ); -} - -/// Called when the xwayland surface is destroyed -fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_destroy", listener.?); - - // Remove listeners that are active for the entire lifetime of the view - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_map.link); - c.wl_list_remove(&self.listen_unmap.link); - - self.view.destroy(); -} - -/// Called when the xwayland surface is mapped, or ready to display on-screen. -fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_map", listener.?); - const view = self.view; - const root = view.output.root; - - // Add listeners that are only active while mapped - self.listen_commit.notify = handleCommit; - c.wl_signal_add(&self.wlr_xwayland_surface.surface.*.events.commit, &self.listen_commit); - - view.wlr_surface = self.wlr_xwayland_surface.surface; - view.floating = false; - - view.natural_width = self.wlr_xwayland_surface.width; - view.natural_height = self.wlr_xwayland_surface.height; - - view.map(); -} - -/// Called when the surface is unmapped and will no longer be displayed. -fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_unmap", listener.?); - - self.view.unmap(); - - // Remove listeners that are only active while mapped - c.wl_list_remove(&self.listen_commit.link); -} - -/// Called when the surface is comitted -/// TODO: check for unexpected change in size and react as needed -fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_commit", listener.?); - const view = self.view; - // See comment in XwaylandView.configure() - if (view.pending_serial != null) { - view.output.root.notifyConfigured(); - view.pending_serial = null; - } -} diff --git a/src/c.zig b/src/c.zig deleted file mode 100644 index df4bcee..0000000 --- a/src/c.zig +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pub usingnamespace @cImport({ - @cDefine("_POSIX_C_SOURCE", "200809L"); - @cDefine("WLR_USE_UNSTABLE", {}); - - @cInclude("time.h"); - @cInclude("stdlib.h"); - @cInclude("wayland-server-core.h"); - //@cInclude("wlr/backend.h"); - //@cInclude("wlr/render/wlr_renderer.h"); - @cInclude("wlr/types/wlr_buffer.h"); - @cInclude("wlr/types/wlr_compositor.h"); - @cInclude("wlr/types/wlr_cursor.h"); - @cInclude("wlr/types/wlr_data_device.h"); - @cInclude("wlr/types/wlr_input_device.h"); - @cInclude("wlr/types/wlr_input_inhibitor.h"); - @cInclude("wlr/types/wlr_keyboard.h"); - @cInclude("wlr/types/wlr_layer_shell_v1.h"); - @cInclude("wlr/types/wlr_matrix.h"); - @cInclude("wlr/types/wlr_output.h"); - @cInclude("wlr/types/wlr_output_layout.h"); - @cInclude("wlr/types/wlr_pointer.h"); - @cInclude("wlr/types/wlr_screencopy_v1.h"); - @cInclude("wlr/types/wlr_seat.h"); - @cInclude("wlr/types/wlr_xcursor_manager.h"); - @cInclude("wlr/types/wlr_xdg_decoration_v1.h"); - @cInclude("wlr/types/wlr_xdg_output_v1.h"); - @cInclude("wlr/types/wlr_xdg_shell.h"); - if (@import("build_options").xwayland) @cInclude("wlr/xwayland.h"); - @cInclude("wlr/util/log.h"); - @cInclude("xkbcommon/xkbcommon.h"); - - // Contains a subset of functions from wlr/backend.h and wlr/render/wlr_renderer.h - // that can be automatically imported - @cInclude("include/bindings.h"); - - @cInclude("river-control-unstable-v1-protocol.h"); -}); - -// These are needed because zig currently names translated anonymous unions -// with a global counter, which makes code unportable. -// See https://github.com/ifreund/river/issues/17 -pub const wlr_xdg_surface_union = @typeInfo(wlr_xdg_surface).Struct.fields[5].name; -pub const wlr_input_device_union = @typeInfo(wlr_input_device).Struct.fields[8].name; diff --git a/src/command.zig b/src/command.zig deleted file mode 100644 index c1db559..0000000 --- a/src/command.zig +++ /dev/null @@ -1,122 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const Seat = @import("Seat.zig"); - -const impl = struct { - const close = @import("command/close.zig").close; - const declareMode = @import("command/declare_mode.zig").declareMode; - const enterMode = @import("command/enter_mode.zig").enterMode; - const exit = @import("command/exit.zig").exit; - const focus = @import("command/focus.zig").focus; - const focusAllTags = @import("command/focus_all_tags.zig").focusAllTags; - const focusOutput = @import("command/focus_output.zig").focusOutput; - const map = @import("command/map.zig").map; - const focusTag = @import("command/focus_tag.zig").focusTag; - const layout = @import("command/layout.zig").layout; - const modMasterCount = @import("command/mod_master_count.zig").modMasterCount; - const modMasterFactor = @import("command/mod_master_factor.zig").modMasterFactor; - const sendToOutput = @import("command/send_to_output.zig").sendToOutput; - const spawn = @import("command/spawn.zig").spawn; - const tagView = @import("command/tag_view.zig").tagView; - const tagViewAllTags = @import("command/tag_view_all_tags.zig").tagViewAllTags; - const toggleFloat = @import("command/toggle_float.zig").toggleFloat; - const toggleTagFocus = @import("command/toggle_tag_focus.zig").toggleTagFocus; - const toggleViewTag = @import("command/toggle_view_tag.zig").toggleViewTag; - const zoom = @import("command/zoom.zig").zoom; -}; - -pub const Direction = enum { - Next, - Prev, - - pub fn parse(str: []const u8) error{InvalidDirection}!Direction { - return if (std.mem.eql(u8, str, "next")) - Direction.Next - else if (std.mem.eql(u8, str, "previous")) - Direction.Prev - else - error.InvalidDirection; - } -}; - -const Definition = struct { - name: []const u8, - impl: fn (*std.mem.Allocator, *Seat, []const []const u8, *[]const u8) Error!void, -}; - -// TODO: this could be replaced with a comptime hashmap -// zig fmt: off -const str_to_impl_fn = [_]Definition{ - .{ .name = "close", .impl = impl.close }, - .{ .name = "declare_mode", .impl = impl.declareMode}, - .{ .name = "enter_mode", .impl = impl.enterMode }, - .{ .name = "exit", .impl = impl.exit }, - .{ .name = "focus", .impl = impl.focus }, - .{ .name = "focus_all_tags", .impl = impl.focusAllTags }, - .{ .name = "focus_output", .impl = impl.focusOutput }, - .{ .name = "focus_tag", .impl = impl.focusTag }, - .{ .name = "layout", .impl = impl.layout }, - .{ .name = "mod_master_count", .impl = impl.modMasterCount }, - .{ .name = "mod_master_factor", .impl = impl.modMasterFactor }, - .{ .name = "send_to_output", .impl = impl.sendToOutput }, - .{ .name = "spawn", .impl = impl.spawn }, - .{ .name = "map", .impl = impl.map }, - .{ .name = "tag_view", .impl = impl.tagView }, - .{ .name = "tag_view_all_tags", .impl = impl.tagViewAllTags }, - .{ .name = "toggle_float", .impl = impl.toggleFloat }, - .{ .name = "toggle_tag_focus", .impl = impl.toggleTagFocus }, - .{ .name = "toggle_view_tag", .impl = impl.toggleViewTag }, - .{ .name = "zoom", .impl = impl.zoom }, -}; -// zig fmt: on - -pub const Error = error{ - NoCommand, - UnknownCommand, - NotEnoughArguments, - TooManyArguments, - Overflow, - InvalidCharacter, - InvalidDirection, - OutOfMemory, - CommandFailed, -}; - -/// Run a command for the given Seat. The `args` parameter is similar to the -/// classic argv in that the command to be run is passed as the first argument. -/// If the command fails with Error.CommandFailed, a failure message will be -/// allocated and the slice pointed to by the `failure_message` parameter will -/// be set to point to it. The caller is responsible for freeing this message -/// in the case of failure. -pub fn run( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len == 0) return Error.NoCommand; - - const name = args[0]; - const impl_fn = for (str_to_impl_fn) |definition| { - if (std.mem.eql(u8, name, definition.name)) break definition.impl; - } else return Error.UnknownCommand; - - try impl_fn(allocator, seat, args, failure_message); -} diff --git a/src/command/close.zig b/src/command/close.zig deleted file mode 100644 index 04b2dcd..0000000 --- a/src/command/close.zig +++ /dev/null @@ -1,37 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Close the focused view, if any. -pub fn close( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (seat.focused_view) |view| { - // Note: we don't call arrange() here as it will be called - // automatically when the view is unmapped. - view.close(); - } -} diff --git a/src/command/declare_mode.zig b/src/command/declare_mode.zig deleted file mode 100644 index 943ede8..0000000 --- a/src/command/declare_mode.zig +++ /dev/null @@ -1,51 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Mapping = @import("../Mapping.zig"); -const Seat = @import("../Seat.zig"); - -/// Declare a new keymap mode -pub fn declareMode( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const config = &seat.input_manager.server.config; - const new_mode_name = args[1]; - - if (config.mode_to_id.get(new_mode_name) != null) { - failure_message.* = try std.fmt.allocPrint( - allocator, - "mode '{}' already exists and cannot be re-declared", - .{new_mode_name}, - ); - return Error.CommandFailed; - } - - try config.mode_to_id.putNoClobber(new_mode_name, config.modes.items.len); - errdefer _ = config.mode_to_id.remove(new_mode_name); - try config.modes.append(std.ArrayList(Mapping).init(allocator)); -} diff --git a/src/command/enter_mode.zig b/src/command/enter_mode.zig deleted file mode 100644 index 8c7e3e4..0000000 --- a/src/command/enter_mode.zig +++ /dev/null @@ -1,43 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Switch to the given mode -pub fn enterMode( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const config = seat.input_manager.server.config; - const target_mode = args[1]; - seat.mode_id = config.mode_to_id.getValue(target_mode) orelse { - failure_message.* = try std.fmt.allocPrint( - allocator, - "cannot enter non-existant mode '{}'", - .{target_mode}, - ); - return Error.CommandFailed; - }; -} diff --git a/src/command/exit.zig b/src/command/exit.zig deleted file mode 100644 index a21e4f3..0000000 --- a/src/command/exit.zig +++ /dev/null @@ -1,34 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Exit the compositor, terminating the wayland session. -pub fn exit( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len > 1) return Error.TooManyArguments; - c.wl_display_terminate(seat.input_manager.server.wl_display); -} diff --git a/src/command/focus.zig b/src/command/focus.zig deleted file mode 100644 index 0e2c9e0..0000000 --- a/src/command/focus.zig +++ /dev/null @@ -1,67 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Direction = @import("../command.zig").Direction; -const Seat = @import("../Seat.zig"); -const View = @import("../View.zig"); -const ViewStack = @import("../view_stack.zig").ViewStack; - -/// Focus either the next or the previous visible view, depending on the enum -/// passed. Does nothing if there are 1 or 0 views in the stack. -pub fn focus( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const direction = try Direction.parse(args[1]); - const output = seat.focused_output; - - if (seat.focused_view) |current_focus| { - // If there is a currently focused view, focus the next visible view in the stack. - const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", current_focus); - var it = switch (direction) { - .Next => ViewStack(View).iterator(focused_node, output.current_focused_tags), - .Prev => ViewStack(View).reverseIterator(focused_node, output.current_focused_tags), - }; - - // Skip past the focused node - _ = it.next(); - // Focus the next visible node if there is one - if (it.next()) |node| { - seat.focus(&node.view); - return; - } - } - - // There is either no currently focused view or the last visible view in the - // stack is focused and we need to wrap. - var it = switch (direction) { - .Next => ViewStack(View).iterator(output.views.first, output.current_focused_tags), - .Prev => ViewStack(View).reverseIterator(output.views.last, output.current_focused_tags), - }; - - seat.focus(if (it.next()) |node| &node.view else null); -} diff --git a/src/command/focus_all_tags.zig b/src/command/focus_all_tags.zig deleted file mode 100644 index 28e001d..0000000 --- a/src/command/focus_all_tags.zig +++ /dev/null @@ -1,32 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Set focus to all tags -pub fn focusAllTags( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - seat.focused_output.pending_focused_tags = 0xFFFFFFFF; - seat.input_manager.server.root.arrange(); -} diff --git a/src/command/focus_output.zig b/src/command/focus_output.zig deleted file mode 100644 index b11664f..0000000 --- a/src/command/focus_output.zig +++ /dev/null @@ -1,54 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Direction = @import("../command.zig").Direction; -const Output = @import("../Output.zig"); -const Seat = @import("../Seat.zig"); - -/// Focus either the next or the previous output, depending on the bool passed. -/// Does nothing if there is only one output. -pub fn focusOutput( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const direction = try Direction.parse(args[1]); - const root = &seat.input_manager.server.root; - // If the noop output is focused, there are no other outputs to switch to - if (seat.focused_output == &root.noop_output) { - std.debug.assert(root.outputs.len == 0); - return; - } - - // Focus the next/prev output in the list if there is one, else wrap - const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output); - seat.focused_output = switch (direction) { - .Next => if (focused_node.next) |node| &node.data else &root.outputs.first.?.data, - .Prev => if (focused_node.prev) |node| &node.data else &root.outputs.last.?.data, - }; - - seat.focus(null); -} diff --git a/src/command/focus_tag.zig b/src/command/focus_tag.zig deleted file mode 100644 index 8f69e6e..0000000 --- a/src/command/focus_tag.zig +++ /dev/null @@ -1,37 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Switch focus to the passed tag. -pub fn focusTag( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const tag = try std.fmt.parseInt(u32, args[1], 10); - const tags = @as(u32, 1) << @intCast(u5, tag - 1); - seat.focused_output.pending_focused_tags = tags; - seat.input_manager.server.root.arrange(); -} diff --git a/src/command/layout.zig b/src/command/layout.zig deleted file mode 100644 index 4a21050..0000000 --- a/src/command/layout.zig +++ /dev/null @@ -1,37 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Leon Henrik Plickat -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -pub fn layout( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - seat.focused_output.layout = seat.focused_output.getLayoutByName(args[1]); - seat.focused_output.arrangeViews(); - seat.input_manager.server.root.startTransaction(); -} diff --git a/src/command/map.zig b/src/command/map.zig deleted file mode 100644 index 90c0b33..0000000 --- a/src/command/map.zig +++ /dev/null @@ -1,110 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Mapping = @import("../Mapping.zig"); -const Seat = @import("../Seat.zig"); - -const modifier_names = [_]struct { - name: []const u8, - modifier: u32, -}{ - .{ .name = "Shift", .modifier = c.WLR_MODIFIER_SHIFT }, - .{ .name = "Lock", .modifier = c.WLR_MODIFIER_CAPS }, - .{ .name = "Control", .modifier = c.WLR_MODIFIER_CTRL }, - .{ .name = "Mod1", .modifier = c.WLR_MODIFIER_ALT }, - .{ .name = "Mod2", .modifier = c.WLR_MODIFIER_MOD2 }, - .{ .name = "Mod3", .modifier = c.WLR_MODIFIER_MOD3 }, - .{ .name = "Mod4", .modifier = c.WLR_MODIFIER_LOGO }, - .{ .name = "Mod5", .modifier = c.WLR_MODIFIER_MOD5 }, -}; - -/// Create a new mapping for a given mode -/// -/// Example: -/// map normal Mod4|Shift Return spawn alacritty -pub fn map( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 4) return Error.NotEnoughArguments; - - // Parse the mode - const config = seat.input_manager.server.config; - const target_mode = args[1]; - const mode_id = config.mode_to_id.getValue(target_mode) orelse { - failure_message.* = try std.fmt.allocPrint( - allocator, - "cannot add mapping to non-existant mode '{}p'", - .{target_mode}, - ); - return Error.CommandFailed; - }; - - // Parse the modifiers - var it = std.mem.split(args[2], "|"); - var modifiers: u32 = 0; - while (it.next()) |mod_name| { - for (modifier_names) |def| { - if (std.mem.eql(u8, def.name, mod_name)) { - modifiers |= def.modifier; - break; - } - } else { - failure_message.* = try std.fmt.allocPrint( - allocator, - "invalid modifier '{}'", - .{mod_name}, - ); - return Error.CommandFailed; - } - } - - // Parse the keysym - const keysym_name = try std.cstr.addNullByte(allocator, args[3]); - defer allocator.free(keysym_name); - const keysym = c.xkb_keysym_from_name(keysym_name, .XKB_KEYSYM_CASE_INSENSITIVE); - if (keysym == c.XKB_KEY_NoSymbol) { - failure_message.* = try std.fmt.allocPrint( - allocator, - "invalid keysym '{}'", - .{args[3]}, - ); - return Error.CommandFailed; - } - - // Check if the mapping already exists - const mode_mappings = &config.modes.items[mode_id]; - for (mode_mappings.items) |existant_mapping| { - if (existant_mapping.modifiers == modifiers and existant_mapping.keysym == keysym) { - failure_message.* = try std.fmt.allocPrint( - allocator, - "a mapping for modifiers '{}' and keysym '{}' already exists", - .{ args[2], args[3] }, - ); - return Error.CommandFailed; - } - } - - try mode_mappings.append(try Mapping.init(allocator, keysym, modifiers, args[4..])); -} diff --git a/src/command/mod_master_count.zig b/src/command/mod_master_count.zig deleted file mode 100644 index 38a379e..0000000 --- a/src/command/mod_master_count.zig +++ /dev/null @@ -1,42 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Modify the number of master views -pub fn modMasterCount( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const delta = try std.fmt.parseInt(i32, args[1], 10); - const output = seat.focused_output; - output.master_count = @intCast( - u32, - std.math.max(0, @intCast(i32, output.master_count) + delta), - ); - seat.input_manager.server.root.arrange(); -} diff --git a/src/command/mod_master_factor.zig b/src/command/mod_master_factor.zig deleted file mode 100644 index ec8065a..0000000 --- a/src/command/mod_master_factor.zig +++ /dev/null @@ -1,45 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Modify the percent of the width of the screen that the master views occupy. -pub fn modMasterFactor( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const delta = try std.fmt.parseFloat(f64, args[1]); - const output = seat.focused_output; - const new_master_factor = std.math.min( - std.math.max(output.master_factor + delta, 0.05), - 0.95, - ); - if (new_master_factor != output.master_factor) { - output.master_factor = new_master_factor; - seat.input_manager.server.root.arrange(); - } -} diff --git a/src/command/send_to_output.zig b/src/command/send_to_output.zig deleted file mode 100644 index de92f6f..0000000 --- a/src/command/send_to_output.zig +++ /dev/null @@ -1,62 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Direction = @import("../command.zig").Direction; -const Output = @import("../Output.zig"); -const Seat = @import("../Seat.zig"); - -/// Send the focused view to the the next or the previous output, depending on -/// the bool passed. Does nothing if there is only one output. -pub fn sendToOutput( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const direction = try Direction.parse(args[1]); - const root = &seat.input_manager.server.root; - - if (seat.focused_view) |view| { - // If the noop output is focused, there is nowhere to send the view - if (view.output == &root.noop_output) { - std.debug.assert(root.outputs.len == 0); - return; - } - - // Send to the next/preg output in the list if there is one, else wrap - const current_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", view.output); - const destination_output = switch (direction) { - .Next => if (current_node.next) |node| &node.data else &root.outputs.first.?.data, - .Prev => if (current_node.prev) |node| &node.data else &root.outputs.last.?.data, - }; - - // Move the view to the target output - view.sendToOutput(destination_output); - - // Handle the change and focus whatever's next in the focus stack - root.arrange(); - seat.focus(null); - } -} diff --git a/src/command/spawn.zig b/src/command/spawn.zig deleted file mode 100644 index 880483a..0000000 --- a/src/command/spawn.zig +++ /dev/null @@ -1,47 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Spawn a program. -pub fn spawn( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - - const cmd = try std.mem.join(allocator, " ", args[1..]); - defer allocator.free(cmd); - - const child_args = [_][]const u8{ "/bin/sh", "-c", cmd }; - const child = try std.ChildProcess.init(&child_args, allocator); - defer child.deinit(); - - std.ChildProcess.spawn(child) catch |err| { - failure_message.* = try std.fmt.allocPrint( - allocator, - "failed to spawn {}: {}.", - .{ cmd, err }, - ); - return Error.CommandFailed; - }; -} diff --git a/src/command/tag_view.zig b/src/command/tag_view.zig deleted file mode 100644 index 735481d..0000000 --- a/src/command/tag_view.zig +++ /dev/null @@ -1,43 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Set the tag of the focused view. -pub fn tagView( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const tag = try std.fmt.parseInt(u32, args[1], 10); - const tags = @as(u32, 1) << @intCast(u5, tag - 1); - if (seat.focused_view) |view| { - if (view.current_tags != tags) { - view.pending_tags = tags; - seat.input_manager.server.root.arrange(); - } - } -} diff --git a/src/command/tag_view_all_tags.zig b/src/command/tag_view_all_tags.zig deleted file mode 100644 index 2d187f3..0000000 --- a/src/command/tag_view_all_tags.zig +++ /dev/null @@ -1,38 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Tag the focused view with all tags. -pub fn tagViewAllTags( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (seat.focused_view) |view| { - if (view.current_tags != 0xFFFFFFFF) { - view.pending_tags = 0xFFFFFFFF; - seat.input_manager.server.root.arrange(); - } - } -} diff --git a/src/command/toggle_float.zig b/src/command/toggle_float.zig deleted file mode 100644 index 25655e8..0000000 --- a/src/command/toggle_float.zig +++ /dev/null @@ -1,38 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Make the focused view float or stop floating, depending on its current -/// state. -pub fn toggleFloat( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len > 1) return Error.TooManyArguments; - if (seat.focused_view) |view| { - view.setFloating(!view.floating); - view.output.root.arrange(); - } -} diff --git a/src/command/toggle_tag_focus.zig b/src/command/toggle_tag_focus.zig deleted file mode 100644 index 09761c7..0000000 --- a/src/command/toggle_tag_focus.zig +++ /dev/null @@ -1,43 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Toggle focus of the passsed tags. -pub fn toggleTagFocus( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const tag = try std.fmt.parseInt(u32, args[1], 10); - const tags = @as(u32, 1) << @intCast(u5, tag - 1); - const output = seat.focused_output; - const new_focused_tags = output.current_focused_tags ^ tags; - if (new_focused_tags != 0) { - output.pending_focused_tags = new_focused_tags; - seat.input_manager.server.root.arrange(); - } -} diff --git a/src/command/toggle_view_tag.zig b/src/command/toggle_view_tag.zig deleted file mode 100644 index 469a750..0000000 --- a/src/command/toggle_view_tag.zig +++ /dev/null @@ -1,44 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); - -/// Toggle the passed tag of the focused view -pub fn toggleViewTag( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len < 2) return Error.NotEnoughArguments; - if (args.len > 2) return Error.TooManyArguments; - - const tag = try std.fmt.parseInt(u32, args[1], 10); - const tags = @as(u32, 1) << @intCast(u5, tag - 1); - if (seat.focused_view) |view| { - const new_tags = view.current_tags ^ tags; - if (new_tags != 0) { - view.pending_tags = new_tags; - seat.input_manager.server.root.arrange(); - } - } -} diff --git a/src/command/zoom.zig b/src/command/zoom.zig deleted file mode 100644 index 59156ab..0000000 --- a/src/command/zoom.zig +++ /dev/null @@ -1,54 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("../c.zig"); - -const Error = @import("../command.zig").Error; -const Seat = @import("../Seat.zig"); -const View = @import("../View.zig"); -const ViewStack = @import("../view_stack.zig").ViewStack; - -/// Bump the focused view to the top of the stack. If the view on the top of -/// the stack is focused, bump the second view to the top. -pub fn zoom( - allocator: *std.mem.Allocator, - seat: *Seat, - args: []const []const u8, - failure_message: *[]const u8, -) Error!void { - if (args.len > 1) return Error.TooManyArguments; - - if (seat.focused_view) |current_focus| { - const output = seat.focused_output; - const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", current_focus); - - var it = ViewStack(View).iterator(output.views.first, output.current_focused_tags); - const zoom_node = if (focused_node == it.next()) - if (it.next()) |second| second else null - else - focused_node; - - if (zoom_node) |to_bump| { - output.views.remove(to_bump); - output.views.push(to_bump); - seat.input_manager.server.root.arrange(); - seat.focus(&to_bump.view); - } - } -} diff --git a/src/log.zig b/src/log.zig deleted file mode 100644 index 30d2564..0000000 --- a/src/log.zig +++ /dev/null @@ -1,41 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -pub const Log = enum { - const Self = @This(); - - Silent = 0, - Error = 1, - Info = 2, - Debug = 3, - - var verbosity = Self.Error; - - pub fn init(_verbosity: Self) void { - verbosity = _verbosity; - } - - fn log(level: Self, comptime format: []const u8, args: var) void { - if (@enumToInt(level) <= @enumToInt(verbosity)) { - // TODO: log the time since start in the same format as wlroots - // TODO: use color if logging to a tty - std.debug.warn("[{}] " ++ format ++ "\n", .{@tagName(level)} ++ args); - } - } -}; diff --git a/src/render.zig b/src/render.zig deleted file mode 100644 index a7055ef..0000000 --- a/src/render.zig +++ /dev/null @@ -1,315 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const build_options = @import("build_options"); -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("Box.zig"); -const LayerSurface = @import("LayerSurface.zig"); -const Output = @import("Output.zig"); -const Server = @import("Server.zig"); -const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; - -const SurfaceRenderData = struct { - output: *const Output, - - /// In output layout coordinates relative to the output - output_x: i32, - output_y: i32, - - when: *c.timespec, -}; - -pub fn renderOutput(output: *Output) void { - const wlr_renderer = output.getRenderer(); - - var now: c.timespec = undefined; - _ = c.clock_gettime(c.CLOCK_MONOTONIC, &now); - - // wlr_output_attach_render makes the OpenGL context current. - if (!c.wlr_output_attach_render(output.wlr_output, null)) { - return; - } - // The "effective" resolution can change if you rotate your outputs. - var width: c_int = undefined; - var height: c_int = undefined; - c.wlr_output_effective_resolution(output.wlr_output, &width, &height); - // Begin the renderer (calls glViewport and some other GL sanity checks) - c.wlr_renderer_begin(wlr_renderer, width, height); - - const color = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }; - c.wlr_renderer_clear(wlr_renderer, &color); - - renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND], &now); - renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM], &now); - - // The first view in the list is "on top" so iterate in reverse. - var it = ViewStack(View).reverseIterator(output.views.last, output.current_focused_tags); - while (it.next()) |node| { - const view = &node.view; - // This check prevents a race condition when a frame is requested - // between mapping of a view and the first configure being handled. - if (view.current_box.width == 0 or view.current_box.height == 0) { - continue; - } - // Floating views are rendered on top of normal views - if (view.floating) { - continue; - } - renderView(output.*, view, &now); - renderBorders(output.*, view, &now); - } - - // Render floating views - it = ViewStack(View).reverseIterator(output.views.last, output.current_focused_tags); - while (it.next()) |node| { - const view = &node.view; - // This check prevents a race condition when a frame is requested - // between mapping of a view and the first configure being handled. - if (view.current_box.width == 0 or view.current_box.height == 0) { - continue; - } - if (!view.floating) { - continue; - } - renderView(output.*, view, &now); - renderBorders(output.*, view, &now); - } - - // Render xwayland unmanged views - if (build_options.xwayland) { - renderXwaylandUnmanaged(output.*, &now); - } - - renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_TOP], &now); - renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY], &now); - - // Hardware cursors are rendered by the GPU on a separate plane, and can be - // moved around without re-rendering what's beneath them - which is more - // efficient. However, not all hardware supports hardware cursors. For this - // reason, wlroots provides a software fallback, which we ask it to render - // here. wlr_cursor handles configuring hardware vs software cursors for you, - // and this function is a no-op when hardware cursors are in use. - c.wlr_output_render_software_cursors(output.wlr_output, null); - - // Conclude rendering and swap the buffers, showing the final frame - // on-screen. - c.wlr_renderer_end(wlr_renderer); - // TODO: handle failure - _ = c.wlr_output_commit(output.wlr_output); -} - -/// Render all surfaces on the passed layer -fn renderLayer(output: Output, layer: std.TailQueue(LayerSurface), now: *c.timespec) void { - var it = layer.first; - while (it) |node| : (it = node.next) { - const layer_surface = &node.data; - var rdata = SurfaceRenderData{ - .output = &output, - .output_x = layer_surface.box.x, - .output_y = layer_surface.box.y, - .when = now, - }; - c.wlr_layer_surface_v1_for_each_surface( - layer_surface.wlr_layer_surface, - renderSurface, - &rdata, - ); - } -} - -fn renderView(output: Output, view: *View, now: *c.timespec) void { - // If we have a stashed buffer, we are in the middle of a transaction - // and need to render that buffer until the transaction is complete. - if (view.stashed_buffer) |buffer| { - var box = c.wlr_box{ - .x = view.current_box.x, - .y = view.current_box.y, - .width = @intCast(c_int, view.current_box.width), - .height = @intCast(c_int, view.current_box.height), - }; - - // Scale the box to the output's current scaling factor - scaleBox(&box, output.wlr_output.scale); - - var matrix: [9]f32 = undefined; - c.wlr_matrix_project_box( - &matrix, - &box, - .WL_OUTPUT_TRANSFORM_NORMAL, - 0.0, - &output.wlr_output.transform_matrix, - ); - - // This takes our matrix, the texture, and an alpha, and performs the actual - // rendering on the GPU. - _ = c.wlr_render_texture_with_matrix( - output.getRenderer(), - buffer.texture, - &matrix, - 1.0, - ); - } else { - // Since there is no stashed buffer, we are not in the middle of - // a transaction and may simply render each toplevel surface. - var rdata = SurfaceRenderData{ - .output = &output, - .output_x = view.current_box.x, - .output_y = view.current_box.y, - .when = now, - }; - - view.forEachSurface(renderSurface, &rdata); - } -} - -/// Render all xwayland unmanaged windows that appear on the output -fn renderXwaylandUnmanaged(output: Output, now: *c.timespec) void { - const root = output.root; - const output_box: *c.wlr_box = c.wlr_output_layout_get_box( - root.wlr_output_layout, - output.wlr_output, - ); - - var it = output.root.xwayland_unmanaged_views.first; - while (it) |node| : (it = node.next) { - const wlr_xwayland_surface = node.data.wlr_xwayland_surface; - - var rdata = SurfaceRenderData{ - .output = &output, - .output_x = wlr_xwayland_surface.x - output_box.x, - .output_y = wlr_xwayland_surface.y - output_box.y, - .when = now, - }; - c.wlr_surface_for_each_surface(wlr_xwayland_surface.surface, renderSurface, &rdata); - } -} - -/// This function is passed to wlroots to render each surface during iteration -fn renderSurface( - _surface: ?*c.wlr_surface, - surface_x: c_int, - surface_y: c_int, - data: ?*c_void, -) callconv(.C) void { - // wlroots says this will never be null - const surface = _surface.?; - const rdata = @ptrCast(*SurfaceRenderData, @alignCast(@alignOf(SurfaceRenderData), data)); - const output = rdata.output; - const wlr_output = output.wlr_output; - - // We first obtain a wlr_texture, which is a GPU resource. wlroots - // automatically handles negotiating these with the client. The underlying - // resource could be an opaque handle passed from the client, or the client - // could have sent a pixel buffer which we copied to the GPU, or a few other - // means. You don't have to worry about this, wlroots takes care of it. - const texture = c.wlr_surface_get_texture(surface); - if (texture == null) { - return; - } - - var box = c.wlr_box{ - .x = rdata.output_x + surface_x, - .y = rdata.output_y + surface_y, - .width = surface.current.width, - .height = surface.current.height, - }; - - // Scale the box to the output's current scaling factor - scaleBox(&box, wlr_output.scale); - - // wlr_matrix_project_box is a helper which takes a box with a desired - // x, y coordinates, width and height, and an output geometry, then - // prepares an orthographic projection and multiplies the necessary - // transforms to produce a model-view-projection matrix. - var matrix: [9]f32 = undefined; - const transform = c.wlr_output_transform_invert(surface.current.transform); - c.wlr_matrix_project_box(&matrix, &box, transform, 0.0, &wlr_output.transform_matrix); - - // This takes our matrix, the texture, and an alpha, and performs the actual - // rendering on the GPU. - _ = c.wlr_render_texture_with_matrix(output.getRenderer(), texture, &matrix, 1.0); - - // This lets the client know that we've displayed that frame and it can - // prepare another one now if it likes. - c.wlr_surface_send_frame_done(surface, rdata.when); -} - -fn renderBorders(output: Output, view: *View, now: *c.timespec) void { - var border: Box = undefined; - const color = if (view.focused) - [_]f32{ 0.57647059, 0.63137255, 0.63137255, 1.0 } // Solarized base1 - else - [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }; // Solarized base01 - const border_width = output.root.server.config.border_width; - - // left and right, covering the corners as well - border.y = view.current_box.y - @intCast(i32, border_width); - border.width = border_width; - border.height = view.current_box.height + border_width * 2; - - // left - border.x = view.current_box.x - @intCast(i32, border_width); - renderRect(output, border, color); - - // right - border.x = view.current_box.x + @intCast(i32, view.current_box.width); - renderRect(output, border, color); - - // top and bottom - border.x = view.current_box.x; - border.width = view.current_box.width; - border.height = border_width; - - // top - border.y = view.current_box.y - @intCast(i32, border_width); - renderRect(output, border, color); - - // bottom border - border.y = view.current_box.y + @intCast(i32, view.current_box.height); - renderRect(output, border, color); -} - -fn renderRect(output: Output, box: Box, color: [4]f32) void { - var wlr_box = box.toWlrBox(); - scaleBox(&wlr_box, output.wlr_output.scale); - c.wlr_render_rect( - output.getRenderer(), - &wlr_box, - &color, - &output.wlr_output.transform_matrix, - ); -} - -/// Scale a wlr_box, taking the possibility of fractional scaling into account. -fn scaleBox(box: *c.wlr_box, scale: f64) void { - box.x = @floatToInt(c_int, @round(@intToFloat(f64, box.x) * scale)); - box.y = @floatToInt(c_int, @round(@intToFloat(f64, box.y) * scale)); - box.width = scaleLength(box.width, box.x, scale); - box.height = scaleLength(box.height, box.x, scale); -} - -/// Scales a width/height. -/// -/// This might seem overly complex, but it needs to work for fractional scaling. -fn scaleLength(length: c_int, offset: c_int, scale: f64) c_int { - return @floatToInt(c_int, @round(@intToFloat(f64, offset + length) * scale) - - @round(@intToFloat(f64, offset) * scale)); -} diff --git a/src/river.zig b/src/river.zig deleted file mode 100644 index a5f5334..0000000 --- a/src/river.zig +++ /dev/null @@ -1,42 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Server = @import("Server.zig"); - -pub fn main() !void { - Log.init(Log.Debug); - c.wlr_log_init(.WLR_ERROR, null); - - Log.Info.log("Initializing server", .{}); - - var server: Server = undefined; - try server.init(std.heap.c_allocator); - defer server.deinit(); - - try server.start(); - - Log.Info.log("Running server...", .{}); - - server.run(); - - Log.Info.log("Shutting down server", .{}); -} diff --git a/src/riverctl.zig b/src/riverctl.zig deleted file mode 100644 index 287c1aa..0000000 --- a/src/riverctl.zig +++ /dev/null @@ -1,109 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const c = @cImport({ - @cInclude("wayland-client.h"); - @cInclude("river-control-unstable-v1-client-protocol.h"); -}); - -const wl_registry_listener = c.wl_registry_listener{ - .global = handleGlobal, - .global_remove = handleGlobalRemove, -}; - -const command_callback_listener = c.zriver_command_callback_v1_listener{ - .success = handleSuccess, - .failure = handleFailure, -}; - -var river_control_optional: ?*c.zriver_control_v1 = null; - -pub fn main() !void { - const wl_display = c.wl_display_connect(null) orelse return error.CantConnectToDisplay; - const wl_registry = c.wl_display_get_registry(wl_display); - - if (c.wl_registry_add_listener(wl_registry, &wl_registry_listener, null) < 0) - return error.FailedToAddListener; - if (c.wl_display_roundtrip(wl_display) < 0) return error.RoundtripFailed; - - const river_control = river_control_optional orelse return error.RiverControlNotAdvertised; - - var command: c.wl_array = undefined; - c.wl_array_init(&command); - var it = std.process.args(); - // Skip our name - _ = it.nextPosix(); - while (it.nextPosix()) |arg| { - // Add one as we need to copy the null terminators as well - var ptr = @ptrCast([*]u8, c.wl_array_add(&command, arg.len + 1) orelse - return error.OutOfMemory); - for (arg) |ch, i| ptr[i] = ch; - ptr[arg.len] = 0; - } - - const command_callback = c.zriver_control_v1_run_command(river_control, &command); - if (c.zriver_command_callback_v1_add_listener( - command_callback, - &command_callback_listener, - null, - ) < 0) return error.FailedToAddListener; - - // Loop until our callback is called and we exit. - while (true) if (c.wl_display_dispatch(wl_display) < 0) return error.DispatchFailed; -} - -fn handleGlobal( - data: ?*c_void, - wl_registry: ?*c.wl_registry, - name: u32, - interface: ?[*:0]const u8, - version: u32, -) callconv(.C) void { - // We only care about the river_control global - if (std.mem.eql( - u8, - std.mem.spanZ(interface.?), - std.mem.spanZ(@ptrCast([*:0]const u8, c.zriver_control_v1_interface.name.?)), - )) { - river_control_optional = @ptrCast( - *c.zriver_control_v1, - c.wl_registry_bind(wl_registry, name, &c.zriver_control_v1_interface, 1), - ); - } -} - -/// Ignore the event -fn handleGlobalRemove(data: ?*c_void, wl_registry: ?*c.wl_registry, name: u32) callconv(.C) void {} - -/// On success we simply exit with a clean exit code -fn handleSuccess(data: ?*c_void, callback: ?*c.zriver_command_callback_v1) callconv(.C) void { - std.os.exit(0); -} - -/// Print the failure message and exit non-zero -fn handleFailure( - data: ?*c_void, - callback: ?*c.zriver_command_callback_v1, - failure_message: ?[*:0]const u8, -) callconv(.C) void { - if (failure_message) |message| { - std.debug.warn("Error: {}\n", .{failure_message}); - } - std.os.exit(1); -} diff --git a/src/test_main.zig b/src/test_main.zig deleted file mode 100644 index 2ed8247..0000000 --- a/src/test_main.zig +++ /dev/null @@ -1,20 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -test "river test suite" { - _ = @import("view_stack.zig"); -} diff --git a/src/view_stack.zig b/src/view_stack.zig deleted file mode 100644 index 0543d03..0000000 --- a/src/view_stack.zig +++ /dev/null @@ -1,406 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const View = @import("View.zig"); - -/// A specialized doubly-linked stack that allows for filtered iteration -/// over the nodes. T must be View or *View. -pub fn ViewStack(comptime T: type) type { - if (!(T == View or T == *View)) { - @compileError("ViewStack: T must be View or *View"); - } - return struct { - const Self = @This(); - - pub const Node = struct { - /// Previous/next nodes in the stack - prev: ?*Node, - next: ?*Node, - - /// The view stored in this node - view: T, - }; - - /// Top/bottom nodes in the stack - first: ?*Node, - last: ?*Node, - - /// Initialize an undefined stack - pub fn init(self: *Self) void { - self.first = null; - self.last = null; - } - - /// Add a node to the top of the stack. - pub fn push(self: *Self, new_node: *Node) void { - // Set the prev/next pointers of the new node - new_node.prev = null; - new_node.next = self.first; - - if (self.first) |first| { - // If the list is not empty, set the prev pointer of the current - // first node to the new node. - first.prev = new_node; - } else { - // If the list is empty set the last pointer to the new node. - self.last = new_node; - } - - // Set the first pointer to the new node - self.first = new_node; - } - - /// Remove a node from the view stack. This removes it from the stack of - /// all views as well as the stack of visible ones. - pub fn remove(self: *Self, target_node: *Node) void { - // Set the previous node/list head to the next pointer - if (target_node.prev) |prev_node| { - prev_node.next = target_node.next; - } else { - self.first = target_node.next; - } - - // Set the next node/list tail to the previous pointer - if (target_node.next) |next_node| { - next_node.prev = target_node.prev; - } else { - self.last = target_node.prev; - } - } - - const Iterator = struct { - it: ?*Node, - tags: u32, - reverse: bool, - pending: bool, - - /// Returns the next node in iteration order, or null if done. - /// This function is horribly ugly, but it's well tested below. - pub fn next(self: *Iterator) ?*Node { - while (self.it) |node| : (self.it = if (self.reverse) node.prev else node.next) { - if (if (self.pending) - if (node.view.pending_tags) |pending_tags| - self.tags & pending_tags != 0 - else - self.tags & node.view.current_tags != 0 - else - self.tags & node.view.current_tags != 0) { - self.it = if (self.reverse) node.prev else node.next; - return node; - } - } - return null; - } - }; - - /// Returns an iterator starting at the passed node and filtered by - /// checking the passed tags against the current tags of each view. - pub fn iterator(start: ?*Node, tags: u32) Iterator { - return Iterator{ - .it = start, - .tags = tags, - .reverse = false, - .pending = false, - }; - } - - /// Returns a reverse iterator starting at the passed node and filtered by - /// checking the passed tags against the current tags of each view. - pub fn reverseIterator(start: ?*Node, tags: u32) Iterator { - return Iterator{ - .it = start, - .tags = tags, - .reverse = true, - .pending = false, - }; - } - - /// Returns an iterator starting at the passed node and filtered by - /// checking the passed tags against the pending tags of each view. - /// If a view has no pending tags, the current tags are used. - pub fn pendingIterator(start: ?*Node, tags: u32) Iterator { - return Iterator{ - .it = start, - .tags = tags, - .reverse = false, - .pending = true, - }; - } - }; -} - -test "push/remove (*View)" { - const testing = @import("std").testing; - - const allocator = testing.allocator; - - var views: ViewStack(*View) = undefined; - views.init(); - - const one = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(one); - const two = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(two); - const three = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(three); - const four = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(four); - const five = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(five); - - views.push(three); // {3} - views.push(one); // {1, 3} - views.push(four); // {4, 1, 3} - views.push(five); // {5, 4, 1, 3} - views.push(two); // {2, 5, 4, 1, 3} - - // Simple insertion - { - var it = views.first; - testing.expect(it == two); - it = it.?.next; - testing.expect(it == five); - it = it.?.next; - testing.expect(it == four); - it = it.?.next; - testing.expect(it == one); - it = it.?.next; - testing.expect(it == three); - it = it.?.next; - - testing.expect(it == null); - - testing.expect(views.first == two); - testing.expect(views.last == three); - } - - // Removal of first - views.remove(two); - { - var it = views.first; - testing.expect(it == five); - it = it.?.next; - testing.expect(it == four); - it = it.?.next; - testing.expect(it == one); - it = it.?.next; - testing.expect(it == three); - it = it.?.next; - - testing.expect(it == null); - - testing.expect(views.first == five); - testing.expect(views.last == three); - } - - // Removal of last - views.remove(three); - { - var it = views.first; - testing.expect(it == five); - it = it.?.next; - testing.expect(it == four); - it = it.?.next; - testing.expect(it == one); - it = it.?.next; - - testing.expect(it == null); - - testing.expect(views.first == five); - testing.expect(views.last == one); - } - - // Remove from middle - views.remove(four); - { - var it = views.first; - testing.expect(it == five); - it = it.?.next; - testing.expect(it == one); - it = it.?.next; - - testing.expect(it == null); - - testing.expect(views.first == five); - testing.expect(views.last == one); - } - - // Reinsertion - views.push(two); - views.push(three); - views.push(four); - { - var it = views.first; - testing.expect(it == four); - it = it.?.next; - testing.expect(it == three); - it = it.?.next; - testing.expect(it == two); - it = it.?.next; - testing.expect(it == five); - it = it.?.next; - testing.expect(it == one); - it = it.?.next; - - testing.expect(it == null); - - testing.expect(views.first == four); - testing.expect(views.last == one); - } - - // Clear - views.remove(four); - views.remove(two); - views.remove(three); - views.remove(one); - views.remove(five); - - testing.expect(views.first == null); - testing.expect(views.last == null); -} - -test "iteration (View)" { - const c = @import("c.zig"); - const testing = @import("std").testing; - - const allocator = testing.allocator; - - var views: ViewStack(View) = undefined; - views.init(); - - const one_a_pb = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(one_a_pb); - one_a_pb.view.current_tags = 1 << 0; - one_a_pb.view.pending_tags = 1 << 1; - - const two_a = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(two_a); - two_a.view.current_tags = 1 << 0; - two_a.view.pending_tags = null; - - const three_b_pa = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(three_b_pa); - three_b_pa.view.current_tags = 1 << 1; - three_b_pa.view.pending_tags = 1 << 0; - - const four_b = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(four_b); - four_b.view.current_tags = 1 << 1; - four_b.view.pending_tags = null; - - const five_b = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(five_b); - five_b.view.current_tags = 1 << 1; - five_b.view.pending_tags = null; - - views.push(three_b_pa); // {3} - views.push(one_a_pb); // {1, 3} - views.push(four_b); // {4, 1, 3} - views.push(five_b); // {5, 4, 1, 3} - views.push(two_a); // {2, 5, 4, 1, 3} - - // Iteration over all tags - { - var it = ViewStack(View).iterator(views.first, 0xFFFFFFFF); - testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); - testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); - testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); - testing.expect(it.next() == null); - } - - // Iteration over 'a' tags - { - var it = ViewStack(View).iterator(views.first, 1 << 0); - testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); - testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); - testing.expect(it.next() == null); - } - - // Iteration over 'b' tags - { - var it = ViewStack(View).iterator(views.first, 1 << 1); - testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); - testing.expect(it.next() == null); - } - - // Iteration over tags that aren't present - { - var it = ViewStack(View).iterator(views.first, 1 << 2); - testing.expect(it.next() == null); - } - - // Reverse iteration over all tags - { - var it = ViewStack(View).reverseIterator(views.last, 0xFFFFFFFF); - testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); - testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); - testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); - testing.expect(it.next() == null); - } - - // Reverse iteration over 'a' tags - { - var it = ViewStack(View).reverseIterator(views.last, 1 << 0); - testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); - testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); - testing.expect(it.next() == null); - } - - // Reverse iteration over 'b' tags - { - var it = ViewStack(View).reverseIterator(views.last, 1 << 1); - testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); - testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); - testing.expect(it.next() == null); - } - - // Reverse iteration over tags that aren't present - { - var it = ViewStack(View).reverseIterator(views.first, 1 << 2); - testing.expect(it.next() == null); - } - - // Iteration over (pending) 'a' tags - { - var it = ViewStack(View).pendingIterator(views.first, 1 << 0); - testing.expect((if (it.next()) |node| &node.view else null) == &two_a.view); - testing.expect((if (it.next()) |node| &node.view else null) == &three_b_pa.view); - testing.expect(it.next() == null); - } - - // Iteration over (pending) 'b' tags - { - var it = ViewStack(View).pendingIterator(views.first, 1 << 1); - testing.expect((if (it.next()) |node| &node.view else null) == &five_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &four_b.view); - testing.expect((if (it.next()) |node| &node.view else null) == &one_a_pb.view); - testing.expect(it.next() == null); - } - - // Iteration over (pending) tags that aren't present - { - var it = ViewStack(View).pendingIterator(views.first, 1 << 2); - testing.expect(it.next() == null); - } -} -- cgit v1.2.3