Split river and riverctl directories
This commit is contained in:
34
river/Box.zig
Normal file
34
river/Box.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
284
river/Config.zig
Normal file
284
river/Config.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
139
river/Control.zig
Normal file
139
river/Control.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
412
river/Cursor.zig
Normal file
412
river/Cursor.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
53
river/Decoration.zig
Normal file
53
river/Decoration.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
57
river/DecorationManager.zig
Normal file
57
river/DecorationManager.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
146
river/InputManager.zig
Normal file
146
river/InputManager.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
180
river/Keyboard.zig
Normal file
180
river/Keyboard.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
196
river/LayerSurface.zig
Normal file
196
river/LayerSurface.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
46
river/Mapping.zig
Normal file
46
river/Mapping.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
744
river/Output.zig
Normal file
744
river/Output.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
247
river/Root.zig
Normal file
247
river/Root.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
312
river/Seat.zig
Normal file
312
river/Seat.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
279
river/Server.zig
Normal file
279
river/Server.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
271
river/View.zig
Normal file
271
river/View.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
48
river/VoidView.zig
Normal file
48
river/VoidView.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
82
river/XdgPopup.zig
Normal file
82
river/XdgPopup.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
201
river/XdgToplevel.zig
Normal file
201
river/XdgToplevel.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
136
river/XwaylandUnmanaged.zig
Normal file
136
river/XwaylandUnmanaged.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
159
river/XwaylandView.zig
Normal file
159
river/XwaylandView.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
60
river/c.zig
Normal file
60
river/c.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
122
river/command.zig
Normal file
122
river/command.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
37
river/command/close.zig
Normal file
37
river/command/close.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
51
river/command/declare_mode.zig
Normal file
51
river/command/declare_mode.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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));
|
||||
}
|
43
river/command/enter_mode.zig
Normal file
43
river/command/enter_mode.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
34
river/command/exit.zig
Normal file
34
river/command/exit.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
67
river/command/focus.zig
Normal file
67
river/command/focus.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
32
river/command/focus_all_tags.zig
Normal file
32
river/command/focus_all_tags.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
54
river/command/focus_output.zig
Normal file
54
river/command/focus_output.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
37
river/command/focus_tag.zig
Normal file
37
river/command/focus_tag.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
37
river/command/layout.zig
Normal file
37
river/command/layout.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
110
river/command/map.zig
Normal file
110
river/command/map.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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..]));
|
||||
}
|
42
river/command/mod_master_count.zig
Normal file
42
river/command/mod_master_count.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
45
river/command/mod_master_factor.zig
Normal file
45
river/command/mod_master_factor.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
62
river/command/send_to_output.zig
Normal file
62
river/command/send_to_output.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
47
river/command/spawn.zig
Normal file
47
river/command/spawn.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
43
river/command/tag_view.zig
Normal file
43
river/command/tag_view.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
38
river/command/tag_view_all_tags.zig
Normal file
38
river/command/tag_view_all_tags.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
38
river/command/toggle_float.zig
Normal file
38
river/command/toggle_float.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
43
river/command/toggle_tag_focus.zig
Normal file
43
river/command/toggle_tag_focus.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
44
river/command/toggle_view_tag.zig
Normal file
44
river/command/toggle_view_tag.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
54
river/command/zoom.zig
Normal file
54
river/command/zoom.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
41
river/log.zig
Normal file
41
river/log.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
42
river/main.zig
Normal file
42
river/main.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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", .{});
|
||||
}
|
315
river/render.zig
Normal file
315
river/render.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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));
|
||||
}
|
20
river/test_main.zig
Normal file
20
river/test_main.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
test "river test suite" {
|
||||
_ = @import("view_stack.zig");
|
||||
}
|
406
river/view_stack.zig
Normal file
406
river/view_stack.zig
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user