Split river and riverctl directories

This commit is contained in:
Isaac Freund
2020-06-01 15:56:50 +02:00
parent 62abfc5ee5
commit 939beef168
49 changed files with 3 additions and 3 deletions

34
river/Box.zig Normal file
View 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
View 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
View 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
View 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
View 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,
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}
}

View 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));
}

View 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
View 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
View 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);
}

View 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();
}

View 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);
}

View 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
View 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
View 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..]));
}

View 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();
}

View 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();
}
}

View 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
View 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;
};
}

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}

View 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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}
}