From e199bcba43174403a578f7a1e92e0a95076495c8 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Sat, 2 May 2020 23:11:56 +0200 Subject: Rename files contatining top level structs This respects the naming convention of snake_case namespaces and TitleCase types. --- src/Box.zig | 34 +++ src/Config.zig | 223 ++++++++++++++ src/Cursor.zig | 403 +++++++++++++++++++++++++ src/Decoration.zig | 53 ++++ src/DecorationManager.zig | 57 ++++ src/InputManager.zig | 146 +++++++++ src/Keyboard.zig | 180 +++++++++++ src/LayerSurface.zig | 192 ++++++++++++ src/Output.zig | 570 +++++++++++++++++++++++++++++++++++ src/Root.zig | 237 +++++++++++++++ src/Seat.zig | 299 ++++++++++++++++++ src/Server.zig | 221 ++++++++++++++ src/View.zig | 200 ++++++++++++ src/XdgPopup.zig | 73 +++++ src/XdgToplevel.zig | 210 +++++++++++++ src/box.zig | 34 --- src/command.zig | 2 +- src/command/close_view.zig | 2 +- src/command/exit_compositor.zig | 2 +- src/command/focus_output.zig | 4 +- src/command/focus_tags.zig | 2 +- src/command/focus_view.zig | 4 +- src/command/modify_master_count.zig | 2 +- src/command/modify_master_factor.zig | 2 +- src/command/send_to_output.zig | 4 +- src/command/set_view_tags.zig | 2 +- src/command/spawn.zig | 2 +- src/command/toggle_float.zig | 2 +- src/command/toggle_tags.zig | 2 +- src/command/toggle_view_tags.zig | 2 +- src/command/zoom.zig | 4 +- src/config.zig | 223 -------------- src/cursor.zig | 403 ------------------------- src/decoration.zig | 53 ---- src/decoration_manager.zig | 57 ---- src/input_manager.zig | 146 --------- src/keyboard.zig | 180 ----------- src/layer_surface.zig | 192 ------------ src/main.zig | 2 +- src/output.zig | 570 ----------------------------------- src/render.zig | 10 +- src/root.zig | 237 --------------- src/seat.zig | 299 ------------------ src/server.zig | 221 -------------- src/view.zig | 200 ------------ src/view_stack.zig | 2 +- src/xdg_popup.zig | 73 ----- src/xdg_toplevel.zig | 210 ------------- 48 files changed, 3124 insertions(+), 3124 deletions(-) create mode 100644 src/Box.zig create mode 100644 src/Config.zig create mode 100644 src/Cursor.zig create mode 100644 src/Decoration.zig create mode 100644 src/DecorationManager.zig create mode 100644 src/InputManager.zig create mode 100644 src/Keyboard.zig create mode 100644 src/LayerSurface.zig create mode 100644 src/Output.zig create mode 100644 src/Root.zig create mode 100644 src/Seat.zig create mode 100644 src/Server.zig create mode 100644 src/View.zig create mode 100644 src/XdgPopup.zig create mode 100644 src/XdgToplevel.zig delete mode 100644 src/box.zig delete mode 100644 src/config.zig delete mode 100644 src/cursor.zig delete mode 100644 src/decoration.zig delete mode 100644 src/decoration_manager.zig delete mode 100644 src/input_manager.zig delete mode 100644 src/keyboard.zig delete mode 100644 src/layer_surface.zig delete mode 100644 src/output.zig delete mode 100644 src/root.zig delete mode 100644 src/seat.zig delete mode 100644 src/server.zig delete mode 100644 src/view.zig delete mode 100644 src/xdg_popup.zig delete mode 100644 src/xdg_toplevel.zig diff --git a/src/Box.zig b/src/Box.zig new file mode 100644 index 0000000..0ba3e63 --- /dev/null +++ b/src/Box.zig @@ -0,0 +1,34 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const c = @import("c.zig"); + +x: i32, +y: i32, +width: u32, +height: u32, + +pub fn toWlrBox(self: Self) c.wlr_box { + return c.wlr_box{ + .x = @intCast(c_int, self.x), + .y = @intCast(c_int, self.y), + .width = @intCast(c_int, self.width), + .height = @intCast(c_int, self.height), + }; +} diff --git a/src/Config.zig b/src/Config.zig new file mode 100644 index 0000000..315a045 --- /dev/null +++ b/src/Config.zig @@ -0,0 +1,223 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); +const command = @import("command.zig"); + +const Server = @import("Server.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, + +const Keybind = struct { + keysym: c.xkb_keysym_t, + modifiers: u32, + command: command.Command, + arg: command.Arg, +}; + +/// All user-defined keybindings +keybinds: std.ArrayList(Keybind), + +/// 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.keybinds = std.ArrayList(Keybind).init(allocator); + self.float_filter = std.ArrayList([*:0]const u8).init(allocator); + + const mod = c.WLR_MODIFIER_LOGO; + + // Mod+Shift+Return to start an instance of alacritty + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_Return, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.spawn, + .arg = .{ .str = "alacritty" }, + }); + + // Mod+Q to close the focused view + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_q, + .modifiers = mod, + .command = command.close_view, + .arg = .{ .none = {} }, + }); + + // Mod+E to exit river + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_e, + .modifiers = mod, + .command = command.exitCompositor, + .arg = .{ .none = {} }, + }); + + // Mod+J and Mod+K to focus the next/previous view in the layout stack + try self.keybinds.append( + Keybind{ + .keysym = c.XKB_KEY_j, + .modifiers = mod, + .command = command.focusView, + .arg = .{ .direction = .Next }, + }, + ); + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_k, + .modifiers = mod, + .command = command.focusView, + .arg = .{ .direction = .Prev }, + }); + + // Mod+Return to bump the focused view to the top of the layout stack, + // making it the new master + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_Return, + .modifiers = mod, + .command = command.zoom, + .arg = .{ .none = {} }, + }); + + // Mod+H and Mod+L to increase/decrease the width of the master column + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_h, + .modifiers = mod, + .command = command.modifyMasterFactor, + .arg = .{ .float = 0.05 }, + }); + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_l, + .modifiers = mod, + .command = command.modifyMasterFactor, + .arg = .{ .float = -0.05 }, + }); + + // Mod+Shift+H and Mod+Shift+L to increment/decrement the number of + // master views in the layout + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_h, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.modifyMasterCount, + .arg = .{ .int = 1 }, + }); + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_l, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.modifyMasterCount, + .arg = .{ .int = -1 }, + }); + + comptime var i = 0; + inline while (i < 9) : (i += 1) { + // Mod+[1-9] to focus tag [1-9] + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_1 + i, + .modifiers = mod, + .command = command.focusTags, + .arg = .{ .uint = 1 << i }, + }); + // Mod+Shift+[1-9] to tag focused view with tag [1-9] + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_1 + i, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.setViewTags, + .arg = .{ .uint = 1 << i }, + }); + // Mod+Ctrl+[1-9] to toggle focus of tag [1-9] + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_1 + i, + .modifiers = mod | c.WLR_MODIFIER_CTRL, + .command = command.toggleTags, + .arg = .{ .uint = 1 << i }, + }); + // Mod+Shift+Ctrl+[1-9] to toggle tag [1-9] of focused view + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_1 + i, + .modifiers = mod | c.WLR_MODIFIER_CTRL | c.WLR_MODIFIER_SHIFT, + .command = command.toggleViewTags, + .arg = .{ .uint = 1 << i }, + }); + } + + // Mod+0 to focus all tags + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_0, + .modifiers = mod, + .command = command.focusTags, + .arg = .{ .uint = 0xFFFFFFFF }, + }); + + // Mod+Shift+0 to tag focused view with all tags + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_0, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.setViewTags, + .arg = .{ .uint = 0xFFFFFFFF }, + }); + + // Mod+Period and Mod+Comma to focus the next/previous output + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_period, + .modifiers = mod, + .command = command.focusOutput, + .arg = .{ .direction = .Next }, + }); + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_comma, + .modifiers = mod, + .command = command.focusOutput, + .arg = .{ .direction = .Prev }, + }); + + // Mod+Shift+Period/Comma to send the focused view to the the + // next/previous output + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_period, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.sendToOutput, + .arg = .{ .direction = .Next }, + }); + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_comma, + .modifiers = mod | c.WLR_MODIFIER_SHIFT, + .command = command.sendToOutput, + .arg = .{ .direction = .Prev }, + }); + + // Mod+Space to toggle float + try self.keybinds.append(Keybind{ + .keysym = c.XKB_KEY_space, + .modifiers = mod, + .command = command.toggleFloat, + .arg = .{ .none = {} }, + }); + + try self.float_filter.append("float"); +} diff --git a/src/Cursor.zig b/src/Cursor.zig new file mode 100644 index 0000000..98e33a6 --- /dev/null +++ b/src/Cursor.zig @@ -0,0 +1,403 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const 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. + // + // TODO: free this, it allocates! + 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. + // + // TODO: free this, it allocates! + 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); + _ = c.wlr_xcursor_manager_load(self.wlr_xcursor_manager, 1); + + 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. + 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; + } + const surface = switch (view.impl) { + .xdg_toplevel => |xdg_toplevel| c.wlr_xdg_surface_surface_at( + xdg_toplevel.wlr_xdg_surface, + ox - @intToFloat(f64, view.current_box.x), + oy - @intToFloat(f64, view.current_box.y), + sx, + sy, + ), + }; + if (surface) |found| { + return found; + } + } + return null; +} diff --git a/src/Decoration.zig b/src/Decoration.zig new file mode 100644 index 0000000..91c5bed --- /dev/null +++ b/src/Decoration.zig @@ -0,0 +1,53 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const DecorationManager = @import("DecorationManager.zig"); + +// TODO: this needs to listen for destroy and free nodes from the deco list +decoration_manager: *DecorationManager, +wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, + +listen_request_mode: c.wl_listener, + +pub fn init( + self: *Self, + decoration_manager: *DecorationManager, + wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, +) void { + self.decoration_manager = decoration_manager; + self.wlr_xdg_toplevel_decoration = wlr_xdg_toplevel_decoration; + + self.listen_request_mode.notify = handleRequestMode; + c.wl_signal_add(&self.wlr_xdg_toplevel_decoration.events.request_mode, &self.listen_request_mode); + + handleRequestMode(&self.listen_request_mode, self.wlr_xdg_toplevel_decoration); +} + +fn handleRequestMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_request_mode", listener.?); + // TODO: we might need to take this configure serial and do a transaction + _ = c.wlr_xdg_toplevel_decoration_v1_set_mode( + self.wlr_xdg_toplevel_decoration, + c.wlr_xdg_toplevel_decoration_v1_mode.WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE, + ); +} diff --git a/src/DecorationManager.zig b/src/DecorationManager.zig new file mode 100644 index 0000000..5a9abb4 --- /dev/null +++ b/src/DecorationManager.zig @@ -0,0 +1,57 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Decoration = @import("Decoration.zig"); +const Server = @import("Server.zig"); + +server: *Server, + +wlr_xdg_decoration_manager: *c.wlr_xdg_decoration_manager_v1, + +decorations: std.SinglyLinkedList(Decoration), + +listen_new_toplevel_decoration: c.wl_listener, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + self.wlr_xdg_decoration_manager = c.wlr_xdg_decoration_manager_v1_create(server.wl_display) orelse + return error.CantCreateWlrXdgDecorationManager; + + self.listen_new_toplevel_decoration.notify = handleNewToplevelDecoration; + c.wl_signal_add( + &self.wlr_xdg_decoration_manager.events.new_toplevel_decoration, + &self.listen_new_toplevel_decoration, + ); +} + +fn handleNewToplevelDecoration(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_toplevel_decoration", listener.?); + const wlr_xdg_toplevel_decoration = @ptrCast( + *c.wlr_xdg_toplevel_decoration_v1, + @alignCast(@alignOf(*c.wlr_xdg_toplevel_decoration_v1), data), + ); + + const node = self.decorations.allocateNode(self.server.allocator) catch unreachable; + node.data.init(self, wlr_xdg_toplevel_decoration); + self.decorations.prepend(node); +} diff --git a/src/InputManager.zig b/src/InputManager.zig new file mode 100644 index 0000000..3a95346 --- /dev/null +++ b/src/InputManager.zig @@ -0,0 +1,146 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Seat = @import("Seat.zig"); +const Server = @import("Server.zig"); + +const default_seat_name = "default"; + +server: *Server, + +wlr_input_inhibit_manager: *c.wlr_input_inhibit_manager, + +seats: std.TailQueue(Seat), +default_seat: *Seat, + +exclusive_client: ?*c.wl_client, + +listen_inhibit_activate: c.wl_listener, +listen_inhibit_deactivate: c.wl_listener, +listen_new_input: c.wl_listener, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + + // This is automatically freed when the display is destroyed + self.wlr_input_inhibit_manager = + c.wlr_input_inhibit_manager_create(server.wl_display) orelse + return error.CantCreateInputInhibitManager; + + self.seats = std.TailQueue(Seat).init(); + + const seat_node = try server.allocator.create(std.TailQueue(Seat).Node); + try seat_node.data.init(self, default_seat_name); + self.default_seat = &seat_node.data; + self.seats.prepend(seat_node); + + self.exclusive_client = null; + + // Set up all listeners + self.listen_inhibit_activate.notify = handleInhibitActivate; + c.wl_signal_add( + &self.wlr_input_inhibit_manager.events.activate, + &self.listen_inhibit_activate, + ); + + self.listen_inhibit_deactivate.notify = handleInhibitDeactivate; + c.wl_signal_add( + &self.wlr_input_inhibit_manager.events.deactivate, + &self.listen_inhibit_deactivate, + ); + + self.listen_new_input.notify = handleNewInput; + c.wl_signal_add(&self.server.wlr_backend.events.new_input, &self.listen_new_input); +} + +pub fn deinit(self: *Self) void { + while (self.seats.pop()) |seat_node| { + seat_node.data.deinit(); + self.server.allocator.destroy(seat_node); + } +} + +/// Must be called whenever a view is unmapped. +pub fn handleViewUnmap(self: Self, view: *View) void { + var it = self.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + seat.handleViewUnmap(view); + } +} + +/// Returns true if input is currently allowed on the passed surface. +pub fn inputAllowed(self: Self, wlr_surface: *c.wlr_surface) bool { + return if (self.exclusive_client) |exclusive_client| + exclusive_client == c.wl_resource_get_client(wlr_surface.resource) + else + true; +} + +fn handleInhibitActivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_inhibit_activate", listener.?); + + Log.Debug.log("Input inhibitor activated", .{}); + + // Clear focus of all seats + var seat_it = self.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + seat_node.data.setFocusRaw(.{ .none = {} }); + } + + self.exclusive_client = self.wlr_input_inhibit_manager.active_client; +} + +fn handleInhibitDeactivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_inhibit_deactivate", listener.?); + + Log.Debug.log("Input inhibitor deactivated", .{}); + + self.exclusive_client = null; + + // Calling arrangeLayers() like this ensures that any top or overlay, + // keyboard-interactive surfaces will re-grab focus. + var output_it = self.server.root.outputs.first; + while (output_it) |output_node| : (output_it = output_node.next) { + output_node.data.arrangeLayers(); + } + + // After ensuring that any possible layer surface focus grab has occured, + // have each Seat handle focus. + var seat_it = self.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + seat_node.data.focus(null); + } +} + +/// This event is raised by the backend when a new input device becomes available. +fn handleNewInput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_input", listener.?); + const device = @ptrCast(*c.wlr_input_device, @alignCast(@alignOf(*c.wlr_input_device), data)); + + // TODO: suport multiple seats + if (self.seats.first) |seat_node| { + seat_node.data.addDevice(device) catch unreachable; + } +} diff --git a/src/Keyboard.zig b/src/Keyboard.zig new file mode 100644 index 0000000..8c19b84 --- /dev/null +++ b/src/Keyboard.zig @@ -0,0 +1,180 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Seat = @import("Seat.zig"); + +seat: *Seat, +wlr_input_device: *c.wlr_input_device, +wlr_keyboard: *c.wlr_keyboard, + +listen_key: c.wl_listener, +listen_modifiers: c.wl_listener, + +pub fn init(self: *Self, seat: *Seat, wlr_input_device: *c.wlr_input_device) !void { + self.seat = seat; + self.wlr_input_device = wlr_input_device; + self.wlr_keyboard = @field(wlr_input_device, c.wlr_input_device_union).keyboard; + + // We need to prepare an XKB keymap and assign it to the keyboard. This + // assumes the defaults (e.g. layout = "us"). + const rules = c.xkb_rule_names{ + .rules = null, + .model = null, + .layout = null, + .variant = null, + .options = null, + }; + const context = c.xkb_context_new(.XKB_CONTEXT_NO_FLAGS) orelse + return error.CantCreateXkbContext; + defer c.xkb_context_unref(context); + + const keymap = c.xkb_keymap_new_from_names( + context, + &rules, + .XKB_KEYMAP_COMPILE_NO_FLAGS, + ) orelse + return error.CantCreateXkbKeymap; + defer c.xkb_keymap_unref(keymap); + + // TODO: handle failure after https://github.com/swaywm/wlroots/pull/2081 + c.wlr_keyboard_set_keymap(self.wlr_keyboard, keymap); + c.wlr_keyboard_set_repeat_info(self.wlr_keyboard, 25, 600); + + // Setup listeners for keyboard events + self.listen_key.notify = handleKey; + c.wl_signal_add(&self.wlr_keyboard.events.key, &self.listen_key); + + self.listen_modifiers.notify = handleModifiers; + c.wl_signal_add(&self.wlr_keyboard.events.modifiers, &self.listen_modifiers); +} + +fn handleKey(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This event is raised when a key is pressed or released. + const self = @fieldParentPtr(Self, "listen_key", listener.?); + const event = @ptrCast( + *c.wlr_event_keyboard_key, + @alignCast(@alignOf(*c.wlr_event_keyboard_key), data), + ); + + const wlr_keyboard = self.wlr_keyboard; + + // Translate libinput keycode -> xkbcommon + const keycode = event.keycode + 8; + + // Get a list of keysyms as xkb reports them + var translated_keysyms: ?[*]c.xkb_keysym_t = undefined; + const translated_keysyms_len = c.xkb_state_key_get_syms( + wlr_keyboard.xkb_state, + keycode, + &translated_keysyms, + ); + + // Get a list of keysyms ignoring modifiers (e.g. 1 instead of !) + // Important for bindings 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.handleBuiltinKeybind(translated_keysyms.?[i])) { + handled = true; + break; + } else if (self.seat.handleKeybinding(translated_keysyms.?[i], modifiers)) { + handled = true; + break; + } + } + if (!handled) { + i = 0; + while (i < raw_keysyms_len) : (i += 1) { + if (self.handleBuiltinKeybind(raw_keysyms.?[i])) { + handled = true; + break; + } else if (self.seat.handleKeybinding(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 bindings such as VT switching. +/// Returns true if the keysym was handled. +fn handleBuiltinKeybind(self: Self, keysym: c.xkb_keysym_t) bool { + if (keysym >= c.XKB_KEY_XF86Switch_VT_1 and keysym <= c.XKB_KEY_XF86Switch_VT_12) { + Log.Debug.log("Switch VT keysym received", .{}); + const wlr_backend = self.seat.input_manager.server.wlr_backend; + if (c.river_wlr_backend_is_multi(wlr_backend)) { + if (c.river_wlr_backend_get_session(wlr_backend)) |session| { + const vt = keysym - c.XKB_KEY_XF86Switch_VT_1 + 1; + Log.Debug.log("Switching to VT {}", .{vt}); + _ = c.wlr_session_change_vt(session, vt); + } + } + return true; + } + return false; +} diff --git a/src/LayerSurface.zig b/src/LayerSurface.zig new file mode 100644 index 0000000..5da7025 --- /dev/null +++ b/src/LayerSurface.zig @@ -0,0 +1,192 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); + +output: *Output, +wlr_layer_surface: *c.wlr_layer_surface_v1, + +/// True if the layer surface is currently mapped +mapped: bool, + +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, + layer: c.zwlr_layer_shell_v1_layer, +) void { + self.output = output; + self.wlr_layer_surface = wlr_layer_surface; + wlr_layer_surface.data = self; + + self.layer = layer; + + // Temporarily set mapped to true and apply the pending state to allow + // for inital arrangement which sends the first configure. + self.mapped = true; + const stashed_state = wlr_layer_surface.current; + wlr_layer_surface.current = wlr_layer_surface.client_pending; + output.arrangeLayers(); + 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.layers[@intCast(usize, @enumToInt(self.layer))].remove(node); + output.root.server.allocator.destroy(node); + + self.output.arrangeLayers(); +} + +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}); + + self.mapped = true; + + // 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, + ); +} + +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 since we set our mapped + // bool to true so that we can avoid making the arrange function even + // more complex. + // + // 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); + } + + self.mapped = false; + + // 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.?); + Log.Debug.log("new layer surface popup.", .{}); + // TODO: handle popups + unreachable; +} diff --git a/src/Output.zig b/src/Output.zig new file mode 100644 index 0000000..a7921f0 --- /dev/null +++ b/src/Output.zig @@ -0,0 +1,570 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); +const render = @import("render.zig"); + +const Box = @import("Box.zig"); +const LayerSurface = @import("LayerSurface.zig"); +const Log = @import("log.zig").Log; +const Root = @import("Root.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +root: *Root, +wlr_output: *c.wlr_output, + +/// All layer surfaces on the output, indexed by the layer enum. +layers: [4]std.TailQueue(LayerSurface), + +/// The area left for views and other layer surfaces after applying the +/// exclusive zones of exclusive layer surfaces. +usable_box: Box, + +/// The top of the stack is the "most important" view. +views: ViewStack(View), + +/// A bit field of focused tags +current_focused_tags: u32, +pending_focused_tags: ?u32, + +/// Number of views in "master" section of the screen. +master_count: u32, + +/// Percentage of the total screen that the master section takes up. +master_factor: f64, + +// All listeners for this output, in alphabetical order +listen_destroy: c.wl_listener, +listen_frame: c.wl_listener, +listen_mode: c.wl_listener, + +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; + + // 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); + } +} + +/// Add a new view to the output. arrangeViews() will be called by the view +/// when it is mapped. +pub fn addView(self: *Self, wlr_xdg_surface: *c.wlr_xdg_surface) void { + const node = self.root.server.allocator.create(ViewStack(View).Node) catch unreachable; + node.view.init_xdg_toplevel(self, self.current_focused_tags, wlr_xdg_surface); + self.views.push(node); +} + +/// Add a newly created layer surface to the output. +pub fn addLayerSurface(self: *Self, wlr_layer_surface: *c.wlr_layer_surface_v1) !void { + const layer = wlr_layer_surface.client_pending.layer; + const node = try self.layers[@intCast(usize, @enumToInt(layer))].allocateNode(self.root.server.allocator); + node.data.init(self, wlr_layer_surface, layer); + self.layers[@intCast(usize, @enumToInt(layer))].append(node); + self.arrangeLayers(); +} + +/// 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; + }; + + 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; + + var master_column_width: u32 = undefined; + var slave_column_width: u32 = undefined; + if (master_count > 0 and slave_count > 0) { + // If both master and slave views are present + master_column_width = @floatToInt(u32, @round(@intToFloat(f64, layout_width) * self.master_factor)); + slave_column_width = layout_width - master_column_width; + } else if (master_count > 0) { + master_column_width = layout_width; + slave_column_width = 0; + } else { + slave_column_width = layout_width; + master_column_width = 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) { + 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_column_width, + .height = master_height + if (i == 0) master_height_rem else 0, + }; + } else { + const slave_height = @divTrunc(layout_height, slave_count); + const slave_height_rem = layout_height % slave_count; + + new_box = .{ + .x = @intCast(i32, master_column_width), + .y = @intCast(i32, (i - master_count) * slave_height + + if (i > master_count) slave_height_rem else 0), + + .width = slave_column_width, + .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; + } +} + +/// 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; + // Only mapped surfaces may gain focus + if (layer_surface.mapped and + 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 (!layer_surface.mapped or 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; + c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); + // We need to move the closing layer surface to the noop output + // since it is not 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; + } + } + + // If any seat has the destroyed output focused, focus the fallback one + var seat_it = root.server.input_manager.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + const seat = &seat_node.data; + if (seat.focused_output == self) { + seat.focused_output = fallback_output; + seat.focus(null); + } + } + + // Remove all listeners + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_frame.link); + c.wl_list_remove(&self.listen_mode.link); + + // Clean up the wlr_output + self.wlr_output.data = null; + + // Remove the destroyed output from the list + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + root.outputs.remove(node); + root.server.allocator.destroy(node); + + // Arrange the root in case evacuated views affect the layout + root.arrange(); +} + +fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + // This function is called every time an output is ready to display a frame, + // generally at the output's refresh rate (e.g. 60Hz). + const self = @fieldParentPtr(Self, "listen_frame", listener.?); + render.renderOutput(self); +} + +fn handleMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_mode", listener.?); + self.arrangeLayers(); + self.root.arrange(); +} diff --git a/src/Root.zig b/src/Root.zig new file mode 100644 index 0000000..8483969 --- /dev/null +++ b/src/Root.zig @@ -0,0 +1,237 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const Server = @import("Server.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +/// 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, + +/// 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); + + self.pending_configures = 0; + + self.transaction_timer = null; +} + +pub fn deinit(self: *Self) void { + while (self.outputs.pop()) |output_node| { + output_node.data.deinit(); + self.server.allocator.destroy(output_node); + } + c.wlr_output_layout_destroy(self.wlr_output_layout); +} + +pub fn addOutput(self: *Self, wlr_output: *c.wlr_output) void { + // TODO: Handle failure + const node = self.outputs.allocateNode(self.server.allocator) catch unreachable; + node.data.init(self, wlr_output) catch unreachable; + self.outputs.append(node); + + // if we previously had no real outputs, move focus from the noop output + // to the new one. + if (self.outputs.len == 1) { + // TODO: move views from the noop output to the new one and focus(null) + var it = self.server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + seat_node.data.focused_output = &self.outputs.first.?.data; + } + } +} + +/// Clear the current focus. +pub fn clearFocus(self: *Self) void { + if (self.focused_view) |view| { + _ = c.wlr_xdg_toplevel_set_activated(view.wlr_xdg_surface, false); + } + self.focused_view = null; +} + +/// Arrange all views on all outputs and then start a transaction. +pub fn arrange(self: *Self) void { + var it = self.outputs.first; + while (it) |output_node| : (it = output_node.next) { + output_node.data.arrangeViews(); + } + self.startTransaction(); +} + +/// Initiate an atomic change to the layout. This change will not be +/// applied until all affected clients ack a configure and commit a buffer. +fn startTransaction(self: *Self) void { + // If a new transaction is started while another is in progress, we need + // to reset the pending count to 0 and clear serials from the views + self.pending_configures = 0; + + // Iterate over all views of all outputs + var output_it = self.outputs.first; + while (output_it) |node| : (output_it = node.next) { + const output = &node.data; + var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); + while (view_it.next()) |view_node| { + const view = &view_node.view; + // Clear the serial in case this transaction is interrupting a prior one. + view.pending_serial = null; + + if (view.needsConfigure()) { + view.configure(); + self.pending_configures += 1; + + // We save the current buffer, so we can send an early + // frame done event to give the client a head start on + // redrawing. + view.sendFrameDone(); + } + + // If there is a saved buffer present, then this transaction is interrupting + // a previous transaction and we should keep the old buffer. + if (view.stashed_buffer == null) { + view.stashBuffer(); + } + } + } + + if (self.pending_configures > 0) { + Log.Debug.log( + "Started transaction with {} pending configures.", + .{self.pending_configures}, + ); + + // TODO: log failure to create timer and commit immediately + self.transaction_timer = c.wl_event_loop_add_timer( + self.server.wl_event_loop, + handleTimeout, + self, + ); + + // Set timeout to 200ms + if (c.wl_event_source_timer_update(self.transaction_timer, 200) == -1) { + // TODO: handle failure + } + } else { + self.commitTransaction(); + } +} + +fn handleTimeout(data: ?*c_void) callconv(.C) c_int { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); + + Log.Error.log("Transaction timed out. Some imperfect frames may be shown.", .{}); + + self.commitTransaction(); + + return 0; +} + +pub fn notifyConfigured(self: *Self) void { + self.pending_configures -= 1; + if (self.pending_configures == 0) { + // Stop the timer, as we didn't timeout + if (c.wl_event_source_timer_update(self.transaction_timer, 0) == -1) { + // TODO: handle failure + } + self.commitTransaction(); + } +} + +/// Apply the pending state and drop stashed buffers. This means that +/// the next frame drawn will be the post-transaction state of the +/// layout. Should only be called after all clients have configured for +/// the new layout. If called early imperfect frames may be drawn. +fn commitTransaction(self: *Self) void { + // TODO: apply damage properly + + // Ensure this is set to 0 to avoid entering invalid state (e.g. if called due to timeout) + self.pending_configures = 0; + + // Iterate over all views of all outputs + var output_it = self.outputs.first; + while (output_it) |output_node| : (output_it = output_node.next) { + const output = &output_node.data; + + // If there were pending focused tags, make them the current focus + if (output.pending_focused_tags) |tags| { + Log.Debug.log( + "changing current focus: {b:0>10} to {b:0>10}", + .{ output.current_focused_tags, tags }, + ); + output.current_focused_tags = tags; + output.pending_focused_tags = null; + } + + var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); + while (view_it.next()) |view_node| { + const view = &view_node.view; + // Ensure that all pending state is cleared + view.pending_serial = null; + if (view.pending_box) |state| { + view.current_box = state; + view.pending_box = null; + } + + // Apply possible pending tags + if (view.pending_tags) |tags| { + view.current_tags = tags; + view.pending_tags = null; + } + + view.dropStashedBuffer(); + } + } + + // Iterate over all seats and update focus + var it = self.server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + seat_node.data.focus(null); + } +} diff --git a/src/Seat.zig b/src/Seat.zig new file mode 100644 index 0000000..4335c00 --- /dev/null +++ b/src/Seat.zig @@ -0,0 +1,299 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const 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), + +/// 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.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 keybinding for the passed keysym and modifiers +/// Returns true if the key was handled +pub fn handleKeybinding(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool { + for (self.input_manager.server.config.keybinds.items) |keybind| { + if (modifiers == keybind.modifiers and keysym == keybind.keysym) { + // Execute the bound command + keybind.command(self, keybind.arg); + return true; + } + } + return false; +} + +/// Add a newly created input device to the seat and update the reported +/// capabilities. +pub fn addDevice(self: *Self, device: *c.wlr_input_device) !void { + switch (device.type) { + .WLR_INPUT_DEVICE_KEYBOARD => self.addKeyboard(device) catch unreachable, + .WLR_INPUT_DEVICE_POINTER => self.addPointer(device), + else => {}, + } + + // We need to let the wlr_seat know what our capabilities are, which is + // communiciated to the client. We always have a cursor, even if + // there are no pointer devices, so we always include that capability. + var caps = @intCast(u32, c.WL_SEAT_CAPABILITY_POINTER); + // if list not empty + if (self.keyboards.len > 0) { + caps |= @intCast(u32, c.WL_SEAT_CAPABILITY_KEYBOARD); + } + c.wlr_seat_set_capabilities(self.wlr_seat, caps); +} + +fn addKeyboard(self: *Self, device: *c.wlr_input_device) !void { + c.wlr_seat_set_keyboard(self.wlr_seat, device); + + const node = try self.keyboards.allocateNode(self.input_manager.server.allocator); + try node.data.init(self, device); + self.keyboards.append(node); +} + +fn addPointer(self: Self, device: *c.struct_wlr_input_device) void { + // We don't do anything special with pointers. All of our pointer handling + // is proxied through wlr_cursor. On another compositor, you might take this + // opportunity to do libinput configuration on the device to set + // acceleration, etc. + c.wlr_cursor_attach_input_device(self.cursor.wlr_cursor, device); +} + +fn handleRequestSetSelection(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_request_set_selection", listener.?); + const event = @ptrCast( + *c.wlr_seat_request_set_selection_event, + @alignCast(@alignOf(*c.wlr_seat_request_set_selection_event), data), + ); + c.wlr_seat_set_selection(self.wlr_seat, event.source, event.serial); +} diff --git a/src/Server.zig b/src/Server.zig new file mode 100644 index 0000000..b255aff --- /dev/null +++ b/src/Server.zig @@ -0,0 +1,221 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Config = @import("Config.zig"); +const DecorationManager = @import("DecorationManager.zig"); +const InputManager = @import("InputManager.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; + +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, +wlr_renderer: *c.wlr_renderer, + +wlr_xdg_shell: *c.wlr_xdg_shell, +wlr_layer_shell: *c.wlr_layer_shell_v1, + +decoration_manager: DecorationManager, +input_manager: InputManager, +root: Root, +config: Config, + +listen_new_output: c.wl_listener, +listen_new_xdg_surface: c.wl_listener, +listen_new_layer_surface: c.wl_listener, + +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. + self.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(self.wlr_renderer, self.wl_display); // orelse + // return error.CantInitWlDisplay; + + self.wlr_xdg_shell = c.wlr_xdg_shell_create(self.wl_display) orelse + return error.CantCreateWlrXdgShell; + + self.wlr_layer_shell = c.wlr_layer_shell_v1_create(self.wl_display) orelse + return error.CantCreateWlrLayerShell; + + 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.config.init(self.allocator); + + // These all free themselves when the wl_display is destroyed + _ = c.wlr_compositor_create(self.wl_display, self.wlr_renderer) orelse + return error.CantCreateWlrCompositor; + _ = 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; + + // Register listeners for events on our globals + self.listen_new_output.notify = handleNewOutput; + c.wl_signal_add(&self.wlr_backend.events.new_output, &self.listen_new_output); + + self.listen_new_xdg_surface.notify = handleNewXdgSurface; + c.wl_signal_add(&self.wlr_xdg_shell.events.new_surface, &self.listen_new_xdg_surface); + + self.listen_new_layer_surface.notify = handleNewLayerSurface; + c.wl_signal_add(&self.wlr_layer_shell.events.new_surface, &self.listen_new_layer_surface); +} + +/// Free allocated memory and clean up +pub fn deinit(self: *Self) void { + // Note: order is important here + c.wl_display_destroy_clients(self.wl_display); + c.wl_display_destroy(self.wl_display); + self.input_manager.deinit(); + self.root.deinit(); +} + +/// 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; + } +} + +/// 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", .{}); + + self.input_manager.default_seat.focused_output.addView(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; + } + } + + const output = @ptrCast(*Output, @alignCast(@alignOf(*Output), wlr_layer_surface.output.*.data)); + output.addLayerSurface(wlr_layer_surface) catch unreachable; +} diff --git a/src/View.zig b/src/View.zig new file mode 100644 index 0000000..51e0b98 --- /dev/null +++ b/src/View.zig @@ -0,0 +1,200 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const Root = @import("Root.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XdgToplevel = @import("XdgToplevel.zig"); + +const ViewImpl = union(enum) { + xdg_toplevel: XdgToplevel, +}; + +/// The implementation of this view +impl: ViewImpl, + +/// 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_xdg_toplevel( + self: *Self, + output: *Output, + tags: u32, + wlr_xdg_surface: *c.wlr_xdg_surface, +) 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; + + self.impl = .{ .xdg_toplevel = undefined }; + self.impl.xdg_toplevel.init(self, wlr_xdg_surface); +} + +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), + } + } 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), + } +} + +/// 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(), + } +} + +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), + } +} diff --git a/src/XdgPopup.zig b/src/XdgPopup.zig new file mode 100644 index 0000000..afc01b3 --- /dev/null +++ b/src/XdgPopup.zig @@ -0,0 +1,73 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const XdgToplevel = @import("XdgToplevel.zig"); + +/// The toplevel this popup is a child of +xdg_toplevel: *XdgToplevel, + +/// 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, xdg_toplevel: *XdgToplevel, wlr_xdg_popup: *c.wlr_xdg_popup) void { + self.xdg_toplevel = xdg_toplevel; + self.wlr_xdg_popup = wlr_xdg_popup; + + // The output box relative to the toplevel parent of the popup + const output = xdg_toplevel.view.output; + var box = c.wlr_output_layout_get_box(output.root.wlr_output_layout, output.wlr_output).*; + box.x -= xdg_toplevel.view.current_box.x; + box.y -= xdg_toplevel.view.current_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 server = self.xdg_toplevel.view.output.root.server; + + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_new_popup.link); + + server.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 server = self.xdg_toplevel.view.output.root.server; + + // This will free itself on destroy + var xdg_popup = server.allocator.create(Self) catch unreachable; + xdg_popup.init(self.xdg_toplevel, wlr_xdg_popup); +} diff --git a/src/XdgToplevel.zig b/src/XdgToplevel.zig new file mode 100644 index 0000000..f1433e6 --- /dev/null +++ b/src/XdgToplevel.zig @@ -0,0 +1,210 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XdgPopup = @import("XdgPopup.zig"); + +/// The view this xdg toplevel implements +view: *View, + +/// The corresponding wlroots object +wlr_xdg_surface: *c.wlr_xdg_surface, + +// Listeners that are always active over the view's lifetime +listen_destroy: c.wl_listener, +listen_map: c.wl_listener, +listen_unmap: c.wl_listener, + +// Listeners that are only active while the view is mapped +listen_commit: c.wl_listener, +listen_new_popup: c.wl_listener, + +pub fn init(self: *Self, view: *View, wlr_xdg_surface: *c.wlr_xdg_surface) void { + self.view = view; + self.wlr_xdg_surface = wlr_xdg_surface; + wlr_xdg_surface.data = self; + + // Inform the xdg toplevel that it is tiled. + // For example this prevents firefox from drawing shadows around itself + _ = c.wlr_xdg_toplevel_set_tiled(self.wlr_xdg_surface, c.WLR_EDGE_LEFT | + c.WLR_EDGE_RIGHT | c.WLR_EDGE_TOP | c.WLR_EDGE_BOTTOM); + + // Add listeners that are active over the view's entire lifetime + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&self.wlr_xdg_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&self.wlr_xdg_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&self.wlr_xdg_surface.events.unmap, &self.listen_unmap); +} + +pub fn configure(self: Self, pending_box: Box) void { + self.view.pending_serial = c.wlr_xdg_toplevel_set_size( + self.wlr_xdg_surface, + pending_box.width, + pending_box.height, + ); +} + +pub fn setActivated(self: Self, activated: bool) void { + _ = c.wlr_xdg_toplevel_set_activated(self.wlr_xdg_surface, activated); +} + +/// Close the view. This will lead to the unmap and destroy events being sent +pub fn close(self: Self) void { + c.wlr_xdg_toplevel_send_close(self.wlr_xdg_surface); +} + +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + c.wlr_xdg_surface_for_each_surface(self.wlr_xdg_surface, iterator, user_data); +} + +/// 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); + + // Remove the view from the stack + const node = @fieldParentPtr(ViewStack(View).Node, "view", self.view); + output.views.remove(node); + output.root.server.allocator.destroy(node); +} + +/// 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); + } + + // 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(view); + } + + c.wlr_surface_send_enter(self.wlr_xdg_surface.surface, view.output.wlr_output); + + root.arrange(); +} + +/// 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.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.view); + } + + root.arrange(); + + // 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 server = self.view.output.root.server; + + // This will free itself on destroy + var xdg_popup = server.allocator.create(XdgPopup) catch unreachable; + xdg_popup.init(self, wlr_xdg_popup); +} diff --git a/src/box.zig b/src/box.zig deleted file mode 100644 index 0ba3e63..0000000 --- a/src/box.zig +++ /dev/null @@ -1,34 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const c = @import("c.zig"); - -x: i32, -y: i32, -width: u32, -height: u32, - -pub fn toWlrBox(self: Self) c.wlr_box { - return c.wlr_box{ - .x = @intCast(c_int, self.x), - .y = @intCast(c_int, self.y), - .width = @intCast(c_int, self.width), - .height = @intCast(c_int, self.height), - }; -} diff --git a/src/command.zig b/src/command.zig index a6b8bec..f559315 100644 --- a/src/command.zig +++ b/src/command.zig @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -const Seat = @import("seat.zig"); +const Seat = @import("Seat.zig"); pub const Direction = enum { Next, diff --git a/src/command/close_view.zig b/src/command/close_view.zig index 1240d09..7bee0b6 100644 --- a/src/command/close_view.zig +++ b/src/command/close_view.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Close the focused view, if any. pub fn close_view(seat: *Seat, arg: Arg) void { diff --git a/src/command/exit_compositor.zig b/src/command/exit_compositor.zig index d6c77f8..7736452 100644 --- a/src/command/exit_compositor.zig +++ b/src/command/exit_compositor.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Exit the compositor, terminating the wayland session. pub fn exitCompositor(seat: *Seat, arg: Arg) void { diff --git a/src/command/focus_output.zig b/src/command/focus_output.zig index 188bd2d..5140943 100644 --- a/src/command/focus_output.zig +++ b/src/command/focus_output.zig @@ -20,8 +20,8 @@ const std = @import("std"); const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Output = @import("../output.zig"); -const Seat = @import("../seat.zig"); +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. diff --git a/src/command/focus_tags.zig b/src/command/focus_tags.zig index 7ccd0e8..aa29b7c 100644 --- a/src/command/focus_tags.zig +++ b/src/command/focus_tags.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Switch focus to the passed tags. pub fn focusTags(seat: *Seat, arg: Arg) void { diff --git a/src/command/focus_view.zig b/src/command/focus_view.zig index 85a2975..225fa5e 100644 --- a/src/command/focus_view.zig +++ b/src/command/focus_view.zig @@ -18,8 +18,8 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); -const View = @import("../view.zig"); +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 diff --git a/src/command/modify_master_count.zig b/src/command/modify_master_count.zig index f03c075..c7a5ac1 100644 --- a/src/command/modify_master_count.zig +++ b/src/command/modify_master_count.zig @@ -20,7 +20,7 @@ const std = @import("std"); const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Modify the number of master views pub fn modifyMasterCount(seat: *Seat, arg: Arg) void { diff --git a/src/command/modify_master_factor.zig b/src/command/modify_master_factor.zig index e0ae8f4..b2af06f 100644 --- a/src/command/modify_master_factor.zig +++ b/src/command/modify_master_factor.zig @@ -20,7 +20,7 @@ const std = @import("std"); const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Modify the percent of the width of the screen that the master views occupy. pub fn modifyMasterFactor(seat: *Seat, arg: Arg) void { diff --git a/src/command/send_to_output.zig b/src/command/send_to_output.zig index bece494..f0658ca 100644 --- a/src/command/send_to_output.zig +++ b/src/command/send_to_output.zig @@ -20,8 +20,8 @@ const std = @import("std"); const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Output = @import("../output.zig"); -const Seat = @import("../seat.zig"); +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. diff --git a/src/command/set_view_tags.zig b/src/command/set_view_tags.zig index 38ae677..562d311 100644 --- a/src/command/set_view_tags.zig +++ b/src/command/set_view_tags.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Set the tags of the focused view. pub fn setViewTags(seat: *Seat, arg: Arg) void { diff --git a/src/command/spawn.zig b/src/command/spawn.zig index b9b9c95..53566fa 100644 --- a/src/command/spawn.zig +++ b/src/command/spawn.zig @@ -21,7 +21,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; const Log = @import("../log.zig").Log; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Spawn a program. pub fn spawn(seat: *Seat, arg: Arg) void { diff --git a/src/command/toggle_float.zig b/src/command/toggle_float.zig index da16270..27e3186 100644 --- a/src/command/toggle_float.zig +++ b/src/command/toggle_float.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Make the focused view float or stop floating, depending on its current /// state. diff --git a/src/command/toggle_tags.zig b/src/command/toggle_tags.zig index 53242f8..2d9f86e 100644 --- a/src/command/toggle_tags.zig +++ b/src/command/toggle_tags.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Toggle focus of the passsed tags. pub fn toggleTags(seat: *Seat, arg: Arg) void { diff --git a/src/command/toggle_view_tags.zig b/src/command/toggle_view_tags.zig index 5ae7874..2f5ba3e 100644 --- a/src/command/toggle_view_tags.zig +++ b/src/command/toggle_view_tags.zig @@ -18,7 +18,7 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); +const Seat = @import("../Seat.zig"); /// Toggle the passed tags of the focused view pub fn toggleViewTags(seat: *Seat, arg: Arg) void { diff --git a/src/command/zoom.zig b/src/command/zoom.zig index 617e4bb..5a5ca93 100644 --- a/src/command/zoom.zig +++ b/src/command/zoom.zig @@ -18,8 +18,8 @@ const c = @import("../c.zig"); const Arg = @import("../command.zig").Arg; -const Seat = @import("../seat.zig"); -const View = @import("../view.zig"); +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 diff --git a/src/config.zig b/src/config.zig deleted file mode 100644 index 6866f80..0000000 --- a/src/config.zig +++ /dev/null @@ -1,223 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); -const command = @import("command.zig"); - -const Server = @import("server.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, - -const Keybind = struct { - keysym: c.xkb_keysym_t, - modifiers: u32, - command: command.Command, - arg: command.Arg, -}; - -/// All user-defined keybindings -keybinds: std.ArrayList(Keybind), - -/// 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.keybinds = std.ArrayList(Keybind).init(allocator); - self.float_filter = std.ArrayList([*:0]const u8).init(allocator); - - const mod = c.WLR_MODIFIER_LOGO; - - // Mod+Shift+Return to start an instance of alacritty - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_Return, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.spawn, - .arg = .{ .str = "alacritty" }, - }); - - // Mod+Q to close the focused view - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_q, - .modifiers = mod, - .command = command.close_view, - .arg = .{ .none = {} }, - }); - - // Mod+E to exit river - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_e, - .modifiers = mod, - .command = command.exitCompositor, - .arg = .{ .none = {} }, - }); - - // Mod+J and Mod+K to focus the next/previous view in the layout stack - try self.keybinds.append( - Keybind{ - .keysym = c.XKB_KEY_j, - .modifiers = mod, - .command = command.focusView, - .arg = .{ .direction = .Next }, - }, - ); - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_k, - .modifiers = mod, - .command = command.focusView, - .arg = .{ .direction = .Prev }, - }); - - // Mod+Return to bump the focused view to the top of the layout stack, - // making it the new master - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_Return, - .modifiers = mod, - .command = command.zoom, - .arg = .{ .none = {} }, - }); - - // Mod+H and Mod+L to increase/decrease the width of the master column - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_h, - .modifiers = mod, - .command = command.modifyMasterFactor, - .arg = .{ .float = 0.05 }, - }); - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_l, - .modifiers = mod, - .command = command.modifyMasterFactor, - .arg = .{ .float = -0.05 }, - }); - - // Mod+Shift+H and Mod+Shift+L to increment/decrement the number of - // master views in the layout - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_h, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.modifyMasterCount, - .arg = .{ .int = 1 }, - }); - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_l, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.modifyMasterCount, - .arg = .{ .int = -1 }, - }); - - comptime var i = 0; - inline while (i < 9) : (i += 1) { - // Mod+[1-9] to focus tag [1-9] - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_1 + i, - .modifiers = mod, - .command = command.focusTags, - .arg = .{ .uint = 1 << i }, - }); - // Mod+Shift+[1-9] to tag focused view with tag [1-9] - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_1 + i, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.setViewTags, - .arg = .{ .uint = 1 << i }, - }); - // Mod+Ctrl+[1-9] to toggle focus of tag [1-9] - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_1 + i, - .modifiers = mod | c.WLR_MODIFIER_CTRL, - .command = command.toggleTags, - .arg = .{ .uint = 1 << i }, - }); - // Mod+Shift+Ctrl+[1-9] to toggle tag [1-9] of focused view - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_1 + i, - .modifiers = mod | c.WLR_MODIFIER_CTRL | c.WLR_MODIFIER_SHIFT, - .command = command.toggleViewTags, - .arg = .{ .uint = 1 << i }, - }); - } - - // Mod+0 to focus all tags - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_0, - .modifiers = mod, - .command = command.focusTags, - .arg = .{ .uint = 0xFFFFFFFF }, - }); - - // Mod+Shift+0 to tag focused view with all tags - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_0, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.setViewTags, - .arg = .{ .uint = 0xFFFFFFFF }, - }); - - // Mod+Period and Mod+Comma to focus the next/previous output - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_period, - .modifiers = mod, - .command = command.focusOutput, - .arg = .{ .direction = .Next }, - }); - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_comma, - .modifiers = mod, - .command = command.focusOutput, - .arg = .{ .direction = .Prev }, - }); - - // Mod+Shift+Period/Comma to send the focused view to the the - // next/previous output - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_period, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.sendToOutput, - .arg = .{ .direction = .Next }, - }); - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_comma, - .modifiers = mod | c.WLR_MODIFIER_SHIFT, - .command = command.sendToOutput, - .arg = .{ .direction = .Prev }, - }); - - // Mod+Space to toggle float - try self.keybinds.append(Keybind{ - .keysym = c.XKB_KEY_space, - .modifiers = mod, - .command = command.toggleFloat, - .arg = .{ .none = {} }, - }); - - try self.float_filter.append("float"); -} diff --git a/src/cursor.zig b/src/cursor.zig deleted file mode 100644 index 9f6fc78..0000000 --- a/src/cursor.zig +++ /dev/null @@ -1,403 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const LayerSurface = @import("layer_surface.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. - // - // TODO: free this, it allocates! - 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. - // - // TODO: free this, it allocates! - 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); - _ = c.wlr_xcursor_manager_load(self.wlr_xcursor_manager, 1); - - 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. - 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; - } - const surface = switch (view.impl) { - .xdg_toplevel => |xdg_toplevel| c.wlr_xdg_surface_surface_at( - xdg_toplevel.wlr_xdg_surface, - ox - @intToFloat(f64, view.current_box.x), - oy - @intToFloat(f64, view.current_box.y), - sx, - sy, - ), - }; - if (surface) |found| { - return found; - } - } - return null; -} diff --git a/src/decoration.zig b/src/decoration.zig deleted file mode 100644 index fd49740..0000000 --- a/src/decoration.zig +++ /dev/null @@ -1,53 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const DecorationManager = @import("decoration_manager.zig"); - -// TODO: this needs to listen for destroy and free nodes from the deco list -decoration_manager: *DecorationManager, -wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, - -listen_request_mode: c.wl_listener, - -pub fn init( - self: *Self, - decoration_manager: *DecorationManager, - wlr_xdg_toplevel_decoration: *c.wlr_xdg_toplevel_decoration_v1, -) void { - self.decoration_manager = decoration_manager; - self.wlr_xdg_toplevel_decoration = wlr_xdg_toplevel_decoration; - - self.listen_request_mode.notify = handleRequestMode; - c.wl_signal_add(&self.wlr_xdg_toplevel_decoration.events.request_mode, &self.listen_request_mode); - - handleRequestMode(&self.listen_request_mode, self.wlr_xdg_toplevel_decoration); -} - -fn handleRequestMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_request_mode", listener.?); - // TODO: we might need to take this configure serial and do a transaction - _ = c.wlr_xdg_toplevel_decoration_v1_set_mode( - self.wlr_xdg_toplevel_decoration, - c.wlr_xdg_toplevel_decoration_v1_mode.WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE, - ); -} diff --git a/src/decoration_manager.zig b/src/decoration_manager.zig deleted file mode 100644 index 5ba11c4..0000000 --- a/src/decoration_manager.zig +++ /dev/null @@ -1,57 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Decoration = @import("decoration.zig"); -const Server = @import("server.zig"); - -server: *Server, - -wlr_xdg_decoration_manager: *c.wlr_xdg_decoration_manager_v1, - -decorations: std.SinglyLinkedList(Decoration), - -listen_new_toplevel_decoration: c.wl_listener, - -pub fn init(self: *Self, server: *Server) !void { - self.server = server; - self.wlr_xdg_decoration_manager = c.wlr_xdg_decoration_manager_v1_create(server.wl_display) orelse - return error.CantCreateWlrXdgDecorationManager; - - self.listen_new_toplevel_decoration.notify = handleNewToplevelDecoration; - c.wl_signal_add( - &self.wlr_xdg_decoration_manager.events.new_toplevel_decoration, - &self.listen_new_toplevel_decoration, - ); -} - -fn handleNewToplevelDecoration(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_toplevel_decoration", listener.?); - const wlr_xdg_toplevel_decoration = @ptrCast( - *c.wlr_xdg_toplevel_decoration_v1, - @alignCast(@alignOf(*c.wlr_xdg_toplevel_decoration_v1), data), - ); - - const node = self.decorations.allocateNode(self.server.allocator) catch unreachable; - node.data.init(self, wlr_xdg_toplevel_decoration); - self.decorations.prepend(node); -} diff --git a/src/input_manager.zig b/src/input_manager.zig deleted file mode 100644 index 31ca0a7..0000000 --- a/src/input_manager.zig +++ /dev/null @@ -1,146 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Seat = @import("seat.zig"); -const Server = @import("server.zig"); - -const default_seat_name = "default"; - -server: *Server, - -wlr_input_inhibit_manager: *c.wlr_input_inhibit_manager, - -seats: std.TailQueue(Seat), -default_seat: *Seat, - -exclusive_client: ?*c.wl_client, - -listen_inhibit_activate: c.wl_listener, -listen_inhibit_deactivate: c.wl_listener, -listen_new_input: c.wl_listener, - -pub fn init(self: *Self, server: *Server) !void { - self.server = server; - - // This is automatically freed when the display is destroyed - self.wlr_input_inhibit_manager = - c.wlr_input_inhibit_manager_create(server.wl_display) orelse - return error.CantCreateInputInhibitManager; - - self.seats = std.TailQueue(Seat).init(); - - const seat_node = try server.allocator.create(std.TailQueue(Seat).Node); - try seat_node.data.init(self, default_seat_name); - self.default_seat = &seat_node.data; - self.seats.prepend(seat_node); - - self.exclusive_client = null; - - // Set up all listeners - self.listen_inhibit_activate.notify = handleInhibitActivate; - c.wl_signal_add( - &self.wlr_input_inhibit_manager.events.activate, - &self.listen_inhibit_activate, - ); - - self.listen_inhibit_deactivate.notify = handleInhibitDeactivate; - c.wl_signal_add( - &self.wlr_input_inhibit_manager.events.deactivate, - &self.listen_inhibit_deactivate, - ); - - self.listen_new_input.notify = handleNewInput; - c.wl_signal_add(&self.server.wlr_backend.events.new_input, &self.listen_new_input); -} - -pub fn deinit(self: *Self) void { - while (self.seats.pop()) |seat_node| { - seat_node.data.deinit(); - self.server.allocator.destroy(seat_node); - } -} - -/// Must be called whenever a view is unmapped. -pub fn handleViewUnmap(self: Self, view: *View) void { - var it = self.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - seat.handleViewUnmap(view); - } -} - -/// Returns true if input is currently allowed on the passed surface. -pub fn inputAllowed(self: Self, wlr_surface: *c.wlr_surface) bool { - return if (self.exclusive_client) |exclusive_client| - exclusive_client == c.wl_resource_get_client(wlr_surface.resource) - else - true; -} - -fn handleInhibitActivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_inhibit_activate", listener.?); - - Log.Debug.log("Input inhibitor activated", .{}); - - // Clear focus of all seats - var seat_it = self.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - seat_node.data.setFocusRaw(.{ .none = {} }); - } - - self.exclusive_client = self.wlr_input_inhibit_manager.active_client; -} - -fn handleInhibitDeactivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_inhibit_deactivate", listener.?); - - Log.Debug.log("Input inhibitor deactivated", .{}); - - self.exclusive_client = null; - - // Calling arrangeLayers() like this ensures that any top or overlay, - // keyboard-interactive surfaces will re-grab focus. - var output_it = self.server.root.outputs.first; - while (output_it) |output_node| : (output_it = output_node.next) { - output_node.data.arrangeLayers(); - } - - // After ensuring that any possible layer surface focus grab has occured, - // have each Seat handle focus. - var seat_it = self.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - seat_node.data.focus(null); - } -} - -/// This event is raised by the backend when a new input device becomes available. -fn handleNewInput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_new_input", listener.?); - const device = @ptrCast(*c.wlr_input_device, @alignCast(@alignOf(*c.wlr_input_device), data)); - - // TODO: suport multiple seats - if (self.seats.first) |seat_node| { - seat_node.data.addDevice(device) catch unreachable; - } -} diff --git a/src/keyboard.zig b/src/keyboard.zig deleted file mode 100644 index 84c63a1..0000000 --- a/src/keyboard.zig +++ /dev/null @@ -1,180 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Seat = @import("seat.zig"); - -seat: *Seat, -wlr_input_device: *c.wlr_input_device, -wlr_keyboard: *c.wlr_keyboard, - -listen_key: c.wl_listener, -listen_modifiers: c.wl_listener, - -pub fn init(self: *Self, seat: *Seat, wlr_input_device: *c.wlr_input_device) !void { - self.seat = seat; - self.wlr_input_device = wlr_input_device; - self.wlr_keyboard = @field(wlr_input_device, c.wlr_input_device_union).keyboard; - - // We need to prepare an XKB keymap and assign it to the keyboard. This - // assumes the defaults (e.g. layout = "us"). - const rules = c.xkb_rule_names{ - .rules = null, - .model = null, - .layout = null, - .variant = null, - .options = null, - }; - const context = c.xkb_context_new(.XKB_CONTEXT_NO_FLAGS) orelse - return error.CantCreateXkbContext; - defer c.xkb_context_unref(context); - - const keymap = c.xkb_keymap_new_from_names( - context, - &rules, - .XKB_KEYMAP_COMPILE_NO_FLAGS, - ) orelse - return error.CantCreateXkbKeymap; - defer c.xkb_keymap_unref(keymap); - - // TODO: handle failure after https://github.com/swaywm/wlroots/pull/2081 - c.wlr_keyboard_set_keymap(self.wlr_keyboard, keymap); - c.wlr_keyboard_set_repeat_info(self.wlr_keyboard, 25, 600); - - // Setup listeners for keyboard events - self.listen_key.notify = handleKey; - c.wl_signal_add(&self.wlr_keyboard.events.key, &self.listen_key); - - self.listen_modifiers.notify = handleModifiers; - c.wl_signal_add(&self.wlr_keyboard.events.modifiers, &self.listen_modifiers); -} - -fn handleKey(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This event is raised when a key is pressed or released. - const self = @fieldParentPtr(Self, "listen_key", listener.?); - const event = @ptrCast( - *c.wlr_event_keyboard_key, - @alignCast(@alignOf(*c.wlr_event_keyboard_key), data), - ); - - const wlr_keyboard = self.wlr_keyboard; - - // Translate libinput keycode -> xkbcommon - const keycode = event.keycode + 8; - - // Get a list of keysyms as xkb reports them - var translated_keysyms: ?[*]c.xkb_keysym_t = undefined; - const translated_keysyms_len = c.xkb_state_key_get_syms( - wlr_keyboard.xkb_state, - keycode, - &translated_keysyms, - ); - - // Get a list of keysyms ignoring modifiers (e.g. 1 instead of !) - // Important for bindings 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.handleBuiltinKeybind(translated_keysyms.?[i])) { - handled = true; - break; - } else if (self.seat.handleKeybinding(translated_keysyms.?[i], modifiers)) { - handled = true; - break; - } - } - if (!handled) { - i = 0; - while (i < raw_keysyms_len) : (i += 1) { - if (self.handleBuiltinKeybind(raw_keysyms.?[i])) { - handled = true; - break; - } else if (self.seat.handleKeybinding(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 bindings such as VT switching. -/// Returns true if the keysym was handled. -fn handleBuiltinKeybind(self: Self, keysym: c.xkb_keysym_t) bool { - if (keysym >= c.XKB_KEY_XF86Switch_VT_1 and keysym <= c.XKB_KEY_XF86Switch_VT_12) { - Log.Debug.log("Switch VT keysym received", .{}); - const wlr_backend = self.seat.input_manager.server.wlr_backend; - if (c.river_wlr_backend_is_multi(wlr_backend)) { - if (c.river_wlr_backend_get_session(wlr_backend)) |session| { - const vt = keysym - c.XKB_KEY_XF86Switch_VT_1 + 1; - Log.Debug.log("Switching to VT {}", .{vt}); - _ = c.wlr_session_change_vt(session, vt); - } - } - return true; - } - return false; -} diff --git a/src/layer_surface.zig b/src/layer_surface.zig deleted file mode 100644 index 33652a3..0000000 --- a/src/layer_surface.zig +++ /dev/null @@ -1,192 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("box.zig"); -const Log = @import("log.zig").Log; -const Output = @import("output.zig"); - -output: *Output, -wlr_layer_surface: *c.wlr_layer_surface_v1, - -/// True if the layer surface is currently mapped -mapped: bool, - -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, - layer: c.zwlr_layer_shell_v1_layer, -) void { - self.output = output; - self.wlr_layer_surface = wlr_layer_surface; - wlr_layer_surface.data = self; - - self.layer = layer; - - // Temporarily set mapped to true and apply the pending state to allow - // for inital arrangement which sends the first configure. - self.mapped = true; - const stashed_state = wlr_layer_surface.current; - wlr_layer_surface.current = wlr_layer_surface.client_pending; - output.arrangeLayers(); - 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.layers[@intCast(usize, @enumToInt(self.layer))].remove(node); - output.root.server.allocator.destroy(node); - - self.output.arrangeLayers(); -} - -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}); - - self.mapped = true; - - // 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, - ); -} - -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 since we set our mapped - // bool to true so that we can avoid making the arrange function even - // more complex. - // - // 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); - } - - self.mapped = false; - - // 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.?); - Log.Debug.log("new layer surface popup.", .{}); - // TODO: handle popups - unreachable; -} diff --git a/src/main.zig b/src/main.zig index e10e4dd..a5f5334 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,7 +20,7 @@ const std = @import("std"); const c = @import("c.zig"); const Log = @import("log.zig").Log; -const Server = @import("server.zig"); +const Server = @import("Server.zig"); pub fn main() !void { Log.init(Log.Debug); diff --git a/src/output.zig b/src/output.zig deleted file mode 100644 index 59d72c8..0000000 --- a/src/output.zig +++ /dev/null @@ -1,570 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); -const render = @import("render.zig"); - -const Box = @import("box.zig"); -const LayerSurface = @import("layer_surface.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, - -// All listeners for this output, in alphabetical order -listen_destroy: c.wl_listener, -listen_frame: c.wl_listener, -listen_mode: c.wl_listener, - -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; - - // 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); - } -} - -/// Add a new view to the output. arrangeViews() will be called by the view -/// when it is mapped. -pub fn addView(self: *Self, wlr_xdg_surface: *c.wlr_xdg_surface) void { - const node = self.root.server.allocator.create(ViewStack(View).Node) catch unreachable; - node.view.init_xdg_toplevel(self, self.current_focused_tags, wlr_xdg_surface); - self.views.push(node); -} - -/// Add a newly created layer surface to the output. -pub fn addLayerSurface(self: *Self, wlr_layer_surface: *c.wlr_layer_surface_v1) !void { - const layer = wlr_layer_surface.client_pending.layer; - const node = try self.layers[@intCast(usize, @enumToInt(layer))].allocateNode(self.root.server.allocator); - node.data.init(self, wlr_layer_surface, layer); - self.layers[@intCast(usize, @enumToInt(layer))].append(node); - self.arrangeLayers(); -} - -/// 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; - }; - - 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; - - var master_column_width: u32 = undefined; - var slave_column_width: u32 = undefined; - if (master_count > 0 and slave_count > 0) { - // If both master and slave views are present - master_column_width = @floatToInt(u32, @round(@intToFloat(f64, layout_width) * self.master_factor)); - slave_column_width = layout_width - master_column_width; - } else if (master_count > 0) { - master_column_width = layout_width; - slave_column_width = 0; - } else { - slave_column_width = layout_width; - master_column_width = 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) { - 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_column_width, - .height = master_height + if (i == 0) master_height_rem else 0, - }; - } else { - const slave_height = @divTrunc(layout_height, slave_count); - const slave_height_rem = layout_height % slave_count; - - new_box = .{ - .x = @intCast(i32, master_column_width), - .y = @intCast(i32, (i - master_count) * slave_height + - if (i > master_count) slave_height_rem else 0), - - .width = slave_column_width, - .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; - } -} - -/// 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; - // Only mapped surfaces may gain focus - if (layer_surface.mapped and - 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 (!layer_surface.mapped or 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; - c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface); - // We need to move the closing layer surface to the noop output - // since it is not 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; - } - } - - // If any seat has the destroyed output focused, focus the fallback one - var seat_it = root.server.input_manager.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - const seat = &seat_node.data; - if (seat.focused_output == self) { - seat.focused_output = fallback_output; - seat.focus(null); - } - } - - // Remove all listeners - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_frame.link); - c.wl_list_remove(&self.listen_mode.link); - - // Clean up the wlr_output - self.wlr_output.data = null; - - // Remove the destroyed output from the list - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); - root.outputs.remove(node); - root.server.allocator.destroy(node); - - // Arrange the root in case evacuated views affect the layout - root.arrange(); -} - -fn handleFrame(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - // This function is called every time an output is ready to display a frame, - // generally at the output's refresh rate (e.g. 60Hz). - const self = @fieldParentPtr(Self, "listen_frame", listener.?); - render.renderOutput(self); -} - -fn handleMode(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_mode", listener.?); - self.arrangeLayers(); - self.root.arrange(); -} diff --git a/src/render.zig b/src/render.zig index 4b22011..ac37e42 100644 --- a/src/render.zig +++ b/src/render.zig @@ -19,11 +19,11 @@ const std = @import("std"); const c = @import("c.zig"); -const Box = @import("box.zig"); -const LayerSurface = @import("layer_surface.zig"); -const Output = @import("output.zig"); -const Server = @import("server.zig"); -const View = @import("view.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; pub fn renderOutput(output: *Output) void { diff --git a/src/root.zig b/src/root.zig deleted file mode 100644 index 43f860d..0000000 --- a/src/root.zig +++ /dev/null @@ -1,237 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Log = @import("log.zig").Log; -const Output = @import("output.zig"); -const Server = @import("server.zig"); -const View = @import("view.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; - -/// 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, - -/// 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); - - self.pending_configures = 0; - - self.transaction_timer = null; -} - -pub fn deinit(self: *Self) void { - while (self.outputs.pop()) |output_node| { - output_node.data.deinit(); - self.server.allocator.destroy(output_node); - } - c.wlr_output_layout_destroy(self.wlr_output_layout); -} - -pub fn addOutput(self: *Self, wlr_output: *c.wlr_output) void { - // TODO: Handle failure - const node = self.outputs.allocateNode(self.server.allocator) catch unreachable; - node.data.init(self, wlr_output) catch unreachable; - self.outputs.append(node); - - // if we previously had no real outputs, move focus from the noop output - // to the new one. - if (self.outputs.len == 1) { - // TODO: move views from the noop output to the new one and focus(null) - var it = self.server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focused_output = &self.outputs.first.?.data; - } - } -} - -/// Clear the current focus. -pub fn clearFocus(self: *Self) void { - if (self.focused_view) |view| { - _ = c.wlr_xdg_toplevel_set_activated(view.wlr_xdg_surface, false); - } - self.focused_view = null; -} - -/// Arrange all views on all outputs and then start a transaction. -pub fn arrange(self: *Self) void { - var it = self.outputs.first; - while (it) |output_node| : (it = output_node.next) { - output_node.data.arrangeViews(); - } - self.startTransaction(); -} - -/// Initiate an atomic change to the layout. This change will not be -/// applied until all affected clients ack a configure and commit a buffer. -fn startTransaction(self: *Self) void { - // If a new transaction is started while another is in progress, we need - // to reset the pending count to 0 and clear serials from the views - self.pending_configures = 0; - - // Iterate over all views of all outputs - var output_it = self.outputs.first; - while (output_it) |node| : (output_it = node.next) { - const output = &node.data; - var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); - while (view_it.next()) |view_node| { - const view = &view_node.view; - // Clear the serial in case this transaction is interrupting a prior one. - view.pending_serial = null; - - if (view.needsConfigure()) { - view.configure(); - self.pending_configures += 1; - - // We save the current buffer, so we can send an early - // frame done event to give the client a head start on - // redrawing. - view.sendFrameDone(); - } - - // If there is a saved buffer present, then this transaction is interrupting - // a previous transaction and we should keep the old buffer. - if (view.stashed_buffer == null) { - view.stashBuffer(); - } - } - } - - if (self.pending_configures > 0) { - Log.Debug.log( - "Started transaction with {} pending configures.", - .{self.pending_configures}, - ); - - // TODO: log failure to create timer and commit immediately - self.transaction_timer = c.wl_event_loop_add_timer( - self.server.wl_event_loop, - handleTimeout, - self, - ); - - // Set timeout to 200ms - if (c.wl_event_source_timer_update(self.transaction_timer, 200) == -1) { - // TODO: handle failure - } - } else { - self.commitTransaction(); - } -} - -fn handleTimeout(data: ?*c_void) callconv(.C) c_int { - const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); - - Log.Error.log("Transaction timed out. Some imperfect frames may be shown.", .{}); - - self.commitTransaction(); - - return 0; -} - -pub fn notifyConfigured(self: *Self) void { - self.pending_configures -= 1; - if (self.pending_configures == 0) { - // Stop the timer, as we didn't timeout - if (c.wl_event_source_timer_update(self.transaction_timer, 0) == -1) { - // TODO: handle failure - } - self.commitTransaction(); - } -} - -/// Apply the pending state and drop stashed buffers. This means that -/// the next frame drawn will be the post-transaction state of the -/// layout. Should only be called after all clients have configured for -/// the new layout. If called early imperfect frames may be drawn. -fn commitTransaction(self: *Self) void { - // TODO: apply damage properly - - // Ensure this is set to 0 to avoid entering invalid state (e.g. if called due to timeout) - self.pending_configures = 0; - - // Iterate over all views of all outputs - var output_it = self.outputs.first; - while (output_it) |output_node| : (output_it = output_node.next) { - const output = &output_node.data; - - // If there were pending focused tags, make them the current focus - if (output.pending_focused_tags) |tags| { - Log.Debug.log( - "changing current focus: {b:0>10} to {b:0>10}", - .{ output.current_focused_tags, tags }, - ); - output.current_focused_tags = tags; - output.pending_focused_tags = null; - } - - var view_it = ViewStack(View).iterator(output.views.first, 0xFFFFFFFF); - while (view_it.next()) |view_node| { - const view = &view_node.view; - // Ensure that all pending state is cleared - view.pending_serial = null; - if (view.pending_box) |state| { - view.current_box = state; - view.pending_box = null; - } - - // Apply possible pending tags - if (view.pending_tags) |tags| { - view.current_tags = tags; - view.pending_tags = null; - } - - view.dropStashedBuffer(); - } - } - - // Iterate over all seats and update focus - var it = self.server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focus(null); - } -} diff --git a/src/seat.zig b/src/seat.zig deleted file mode 100644 index b69c1e9..0000000 --- a/src/seat.zig +++ /dev/null @@ -1,299 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Cursor = @import("cursor.zig"); -const InputManager = @import("input_manager.zig"); -const Keyboard = @import("keyboard.zig"); -const LayerSurface = @import("layer_surface.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), - -/// 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.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 keybinding for the passed keysym and modifiers -/// Returns true if the key was handled -pub fn handleKeybinding(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32) bool { - for (self.input_manager.server.config.keybinds.items) |keybind| { - if (modifiers == keybind.modifiers and keysym == keybind.keysym) { - // Execute the bound command - keybind.command(self, keybind.arg); - return true; - } - } - return false; -} - -/// Add a newly created input device to the seat and update the reported -/// capabilities. -pub fn addDevice(self: *Self, device: *c.wlr_input_device) !void { - switch (device.type) { - .WLR_INPUT_DEVICE_KEYBOARD => self.addKeyboard(device) catch unreachable, - .WLR_INPUT_DEVICE_POINTER => self.addPointer(device), - else => {}, - } - - // We need to let the wlr_seat know what our capabilities are, which is - // communiciated to the client. We always have a cursor, even if - // there are no pointer devices, so we always include that capability. - var caps = @intCast(u32, c.WL_SEAT_CAPABILITY_POINTER); - // if list not empty - if (self.keyboards.len > 0) { - caps |= @intCast(u32, c.WL_SEAT_CAPABILITY_KEYBOARD); - } - c.wlr_seat_set_capabilities(self.wlr_seat, caps); -} - -fn addKeyboard(self: *Self, device: *c.wlr_input_device) !void { - c.wlr_seat_set_keyboard(self.wlr_seat, device); - - const node = try self.keyboards.allocateNode(self.input_manager.server.allocator); - try node.data.init(self, device); - self.keyboards.append(node); -} - -fn addPointer(self: Self, device: *c.struct_wlr_input_device) void { - // We don't do anything special with pointers. All of our pointer handling - // is proxied through wlr_cursor. On another compositor, you might take this - // opportunity to do libinput configuration on the device to set - // acceleration, etc. - c.wlr_cursor_attach_input_device(self.cursor.wlr_cursor, device); -} - -fn handleRequestSetSelection(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { - const self = @fieldParentPtr(Self, "listen_request_set_selection", listener.?); - const event = @ptrCast( - *c.wlr_seat_request_set_selection_event, - @alignCast(@alignOf(*c.wlr_seat_request_set_selection_event), data), - ); - c.wlr_seat_set_selection(self.wlr_seat, event.source, event.serial); -} diff --git a/src/server.zig b/src/server.zig deleted file mode 100644 index b0f7c5a..0000000 --- a/src/server.zig +++ /dev/null @@ -1,221 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Config = @import("config.zig"); -const DecorationManager = @import("decoration_manager.zig"); -const InputManager = @import("input_manager.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; - -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, -wlr_renderer: *c.wlr_renderer, - -wlr_xdg_shell: *c.wlr_xdg_shell, -wlr_layer_shell: *c.wlr_layer_shell_v1, - -decoration_manager: DecorationManager, -input_manager: InputManager, -root: Root, -config: Config, - -listen_new_output: c.wl_listener, -listen_new_xdg_surface: c.wl_listener, -listen_new_layer_surface: c.wl_listener, - -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. - self.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(self.wlr_renderer, self.wl_display); // orelse - // return error.CantInitWlDisplay; - - self.wlr_xdg_shell = c.wlr_xdg_shell_create(self.wl_display) orelse - return error.CantCreateWlrXdgShell; - - self.wlr_layer_shell = c.wlr_layer_shell_v1_create(self.wl_display) orelse - return error.CantCreateWlrLayerShell; - - 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.config.init(self.allocator); - - // These all free themselves when the wl_display is destroyed - _ = c.wlr_compositor_create(self.wl_display, self.wlr_renderer) orelse - return error.CantCreateWlrCompositor; - _ = 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; - - // Register listeners for events on our globals - self.listen_new_output.notify = handleNewOutput; - c.wl_signal_add(&self.wlr_backend.events.new_output, &self.listen_new_output); - - self.listen_new_xdg_surface.notify = handleNewXdgSurface; - c.wl_signal_add(&self.wlr_xdg_shell.events.new_surface, &self.listen_new_xdg_surface); - - self.listen_new_layer_surface.notify = handleNewLayerSurface; - c.wl_signal_add(&self.wlr_layer_shell.events.new_surface, &self.listen_new_layer_surface); -} - -/// Free allocated memory and clean up -pub fn deinit(self: *Self) void { - // Note: order is important here - c.wl_display_destroy_clients(self.wl_display); - c.wl_display_destroy(self.wl_display); - self.input_manager.deinit(); - self.root.deinit(); -} - -/// 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; - } -} - -/// 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", .{}); - - self.input_manager.default_seat.focused_output.addView(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; - } - } - - const output = @ptrCast(*Output, @alignCast(@alignOf(*Output), wlr_layer_surface.output.*.data)); - output.addLayerSurface(wlr_layer_surface) catch unreachable; -} diff --git a/src/view.zig b/src/view.zig deleted file mode 100644 index bc024f5..0000000 --- a/src/view.zig +++ /dev/null @@ -1,200 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("box.zig"); -const Log = @import("log.zig").Log; -const Output = @import("output.zig"); -const Root = @import("root.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const XdgToplevel = @import("xdg_toplevel.zig"); - -const ViewImpl = union(enum) { - xdg_toplevel: XdgToplevel, -}; - -/// The implementation of this view -impl: ViewImpl, - -/// 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_xdg_toplevel( - self: *Self, - output: *Output, - tags: u32, - wlr_xdg_surface: *c.wlr_xdg_surface, -) 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; - - self.impl = .{ .xdg_toplevel = undefined }; - self.impl.xdg_toplevel.init(self, wlr_xdg_surface); -} - -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), - } - } 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), - } -} - -/// 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(), - } -} - -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), - } -} diff --git a/src/view_stack.zig b/src/view_stack.zig index 41ead61..7e32a91 100644 --- a/src/view_stack.zig +++ b/src/view_stack.zig @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -const View = @import("view.zig"); +const View = @import("View.zig"); /// A specialized doubly-linked stack that allows for filtered iteration /// over the nodes. T must be View or *View. diff --git a/src/xdg_popup.zig b/src/xdg_popup.zig deleted file mode 100644 index ca06e5d..0000000 --- a/src/xdg_popup.zig +++ /dev/null @@ -1,73 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const XdgToplevel = @import("xdg_toplevel.zig"); - -/// The toplevel this popup is a child of -xdg_toplevel: *XdgToplevel, - -/// 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, xdg_toplevel: *XdgToplevel, wlr_xdg_popup: *c.wlr_xdg_popup) void { - self.xdg_toplevel = xdg_toplevel; - self.wlr_xdg_popup = wlr_xdg_popup; - - // The output box relative to the toplevel parent of the popup - const output = xdg_toplevel.view.output; - var box = c.wlr_output_layout_get_box(output.root.wlr_output_layout, output.wlr_output).*; - box.x -= xdg_toplevel.view.current_box.x; - box.y -= xdg_toplevel.view.current_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 server = self.xdg_toplevel.view.output.root.server; - - c.wl_list_remove(&self.listen_destroy.link); - c.wl_list_remove(&self.listen_new_popup.link); - - server.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 server = self.xdg_toplevel.view.output.root.server; - - // This will free itself on destroy - var xdg_popup = server.allocator.create(Self) catch unreachable; - xdg_popup.init(self.xdg_toplevel, wlr_xdg_popup); -} diff --git a/src/xdg_toplevel.zig b/src/xdg_toplevel.zig deleted file mode 100644 index a0153d4..0000000 --- a/src/xdg_toplevel.zig +++ /dev/null @@ -1,210 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 Isaac Freund -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -const Self = @This(); - -const std = @import("std"); - -const c = @import("c.zig"); - -const Box = @import("box.zig"); -const Log = @import("log.zig").Log; -const View = @import("view.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; -const XdgPopup = @import("xdg_popup.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); -} - -/// 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); - - // Remove the view from the stack - const node = @fieldParentPtr(ViewStack(View).Node, "view", self.view); - output.views.remove(node); - output.root.server.allocator.destroy(node); -} - -/// 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); - } - - // 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(view); - } - - c.wlr_surface_send_enter(self.wlr_xdg_surface.surface, view.output.wlr_output); - - root.arrange(); -} - -/// 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.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.view); - } - - root.arrange(); - - // 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 server = self.view.output.root.server; - - // This will free itself on destroy - var xdg_popup = server.allocator.create(XdgPopup) catch unreachable; - xdg_popup.init(self, wlr_xdg_popup); -} -- cgit v1.2.3