river/river/View.zig
2023-01-08 16:21:42 +01:00

617 lines
22 KiB
Zig

// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 The River Developers
//
// 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, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Self = @This();
const build_options = @import("build_options");
const std = @import("std");
const assert = std.debug.assert;
const math = std.math;
const os = std.os;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
const server = &@import("main.zig").server;
const util = @import("util.zig");
const Output = @import("Output.zig");
const Seat = @import("Seat.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
const XdgToplevel = @import("XdgToplevel.zig");
const XwaylandView = @import("XwaylandView.zig");
const log = std.log.scoped(.view);
pub const Constraints = struct {
min_width: u31,
max_width: u31,
min_height: u31,
max_height: u31,
};
const Impl = union(enum) {
xdg_toplevel: XdgToplevel,
xwayland_view: if (build_options.xwayland) XwaylandView else noreturn,
};
const State = struct {
/// The output-relative effective coordinates and effective dimensions of the view. The
/// surface itself may have other dimensions which are stored in the
/// surface_box member.
box: wlr.Box = wlr.Box{ .x = 0, .y = 0, .width = 0, .height = 0 },
/// The tags of the view, as a bitmask
tags: u32,
/// Number of seats currently focusing the view
focus: u32 = 0,
float: bool = false,
fullscreen: bool = false,
urgent: bool = false,
};
const SavedBuffer = struct {
client_buffer: *wlr.ClientBuffer,
/// x/y relative to the root surface in the surface tree.
surface_box: wlr.Box,
source_box: wlr.FBox,
transform: wl.Output.Transform,
};
/// The implementation of this view
impl: Impl,
/// The output this view is currently associated with
output: *Output,
/// This is non-null exactly when the view is mapped
surface: ?*wlr.Surface = null,
/// This indicates that the view should be destroyed when the current
/// transaction completes. See View.destroy()
destroying: bool = false,
/// The double-buffered state of the view
current: State,
pending: State,
/// The serial sent with the currently pending configure event
pending_serial: ?u32 = null,
/// The currently commited geometry of the surface. The x/y may be negative if
/// for example the client has decided to draw CSD shadows a la GTK.
surface_box: wlr.Box = undefined,
/// The geometry the view's surface had when the transaction started and
/// buffers were saved.
saved_surface_box: wlr.Box = undefined,
/// These are what we render while a transaction is in progress
saved_buffers: std.ArrayListUnmanaged(SavedBuffer) = .{},
/// The floating dimensions the view, saved so that they can be restored if the
/// view returns to floating mode.
float_box: wlr.Box = undefined,
/// This state exists purely to allow for more intuitive behavior when
/// exiting fullscreen if there is no active layout.
post_fullscreen_box: wlr.Box = undefined,
draw_borders: bool = true,
/// This is created when the view is mapped and destroyed when unmapped
foreign_toplevel_handle: ?*wlr.ForeignToplevelHandleV1 = null,
foreign_activate: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated) =
wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated).init(handleForeignActivate),
foreign_fullscreen: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen) =
wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen).init(handleForeignFullscreen),
foreign_close: wl.Listener(*wlr.ForeignToplevelHandleV1) =
wl.Listener(*wlr.ForeignToplevelHandleV1).init(handleForeignClose),
request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) =
wl.Listener(*wlr.XdgActivationV1.event.RequestActivate).init(handleRequestActivate),
pub fn init(self: *Self, output: *Output, impl: Impl) void {
const initial_tags = blk: {
const tags = output.current.tags & server.config.spawn_tagmask;
break :blk if (tags != 0) tags else output.current.tags;
};
self.* = .{
.impl = impl,
.output = output,
.current = .{ .tags = initial_tags },
.pending = .{ .tags = initial_tags },
};
}
/// If saved buffers of the view are currently in use by a transaction,
/// mark this view for destruction when the transaction completes. Otherwise
/// destroy immediately.
pub fn destroy(self: *Self) void {
assert(self.surface == null);
self.destroying = true;
// If there are still saved buffers, then this view needs to be kept
// around until the current transaction completes. This function will be
// called again in Root.commitTransaction()
if (self.saved_buffers.items.len == 0) {
self.saved_buffers.deinit(util.gpa);
const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
util.gpa.destroy(node);
}
}
/// Handle changes to pending state and start a transaction to apply them
pub fn applyPending(self: *Self) void {
if (self.current.float and !self.pending.float) {
// If switching from float to non-float, save the dimensions.
self.float_box = self.current.box;
} else if (!self.current.float and self.pending.float) {
// If switching from non-float to float, apply the saved float dimensions.
self.pending.box = self.float_box;
}
if (!self.lastSetFullscreenState() and self.pending.fullscreen) {
// If switching to fullscreen, set the dimensions to the full area of the output
self.setFullscreen(true);
self.post_fullscreen_box = self.current.box;
self.pending.box = .{
.x = 0,
.y = 0,
.width = undefined,
.height = undefined,
};
self.output.wlr_output.effectiveResolution(&self.pending.box.width, &self.pending.box.height);
} else if (self.lastSetFullscreenState() and !self.pending.fullscreen) {
self.setFullscreen(false);
self.pending.box = self.post_fullscreen_box;
}
// We always need to arrange the output, as there could already be a
// transaction in progress. If we were able to check against the state
// that was pending when that transaction was started, we could in some
// cases avoid the arrangeViews() call here, but we don't store that
// information and it's simpler to always arrange anyways.
self.output.arrangeViews();
server.root.startTransaction();
}
pub fn needsConfigure(self: Self) bool {
return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.needsConfigure(),
.xwayland_view => |xwayland_view| xwayland_view.needsConfigure(),
};
}
pub fn configure(self: *Self) void {
switch (self.impl) {
.xdg_toplevel => |*xdg_toplevel| xdg_toplevel.configure(),
.xwayland_view => |*xwayland_view| {
// TODO(zig): remove this uneeded if statement
// https://github.com/ziglang/zig/issues/13655
if (build_options.xwayland) xwayland_view.configure();
},
}
}
fn lastSetFullscreenState(self: Self) bool {
return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.lastSetFullscreenState(),
.xwayland_view => |xwayland_view| xwayland_view.lastSetFullscreenState(),
};
}
pub fn sendFrameDone(self: Self) void {
var now: os.timespec = undefined;
os.clock_gettime(os.CLOCK.MONOTONIC, &now) catch @panic("CLOCK_MONOTONIC not supported");
self.surface.?.sendFrameDone(&now);
}
pub fn dropSavedBuffers(self: *Self) void {
for (self.saved_buffers.items) |buffer| buffer.client_buffer.base.unlock();
self.saved_buffers.items.len = 0;
}
pub fn saveBuffers(self: *Self) void {
assert(self.saved_buffers.items.len == 0);
self.saved_surface_box = self.surface_box;
self.forEachSurface(*std.ArrayListUnmanaged(SavedBuffer), saveBuffersIterator, &self.saved_buffers);
}
fn saveBuffersIterator(
surface: *wlr.Surface,
surface_x: c_int,
surface_y: c_int,
saved_buffers: *std.ArrayListUnmanaged(SavedBuffer),
) void {
if (surface.buffer) |buffer| {
var source_box: wlr.FBox = undefined;
surface.getBufferSourceBox(&source_box);
saved_buffers.append(util.gpa, .{
.client_buffer = buffer,
.surface_box = .{
.x = surface_x,
.y = surface_y,
.width = surface.current.width,
.height = surface.current.height,
},
.source_box = source_box,
.transform = surface.current.transform,
}) catch return;
_ = buffer.base.lock();
}
}
/// 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.attach(node, server.config.attach_mode);
self.output.sendViewTags();
destination_output.sendViewTags();
if (self.pending.urgent) {
self.output.sendUrgentTags();
destination_output.sendUrgentTags();
}
// if the view is mapped send enter/leave events
if (self.surface != null) {
self.sendLeave(self.output);
self.sendEnter(destination_output);
// Must be present if surface is non-null indicating that the view
// is mapped.
self.foreign_toplevel_handle.?.outputLeave(self.output.wlr_output);
self.foreign_toplevel_handle.?.outputEnter(destination_output.wlr_output);
}
self.output = destination_output;
var output_width: i32 = undefined;
var output_height: i32 = undefined;
destination_output.wlr_output.effectiveResolution(&output_width, &output_height);
if (self.pending.float) {
// Adapt dimensions of view to new output. Only necessary when floating,
// because for tiled views the output will be rearranged, taking care
// of this.
if (self.pending.fullscreen) self.pending.box = self.post_fullscreen_box;
const border_width = if (self.draw_borders) server.config.border_width else 0;
self.pending.box.width = math.min(self.pending.box.width, output_width - (2 * border_width));
self.pending.box.height = math.min(self.pending.box.height, output_height - (2 * border_width));
// Adjust position of view so that it is fully inside the target output.
self.move(0, 0);
}
if (self.pending.fullscreen) {
// If the view is floating, we need to set the post_fullscreen_box, as
// that is still set for the previous output.
if (self.pending.float) self.post_fullscreen_box = self.pending.box;
self.pending.box = .{
.x = 0,
.y = 0,
.width = output_width,
.height = output_height,
};
}
}
fn sendEnter(self: *Self, output: *Output) void {
self.forEachSurface(*wlr.Output, sendEnterIterator, output.wlr_output);
}
fn sendEnterIterator(surface: *wlr.Surface, _: c_int, _: c_int, wlr_output: *wlr.Output) void {
surface.sendEnter(wlr_output);
}
fn sendLeave(self: *Self, output: *Output) void {
self.forEachSurface(*wlr.Output, sendLeaveIterator, output.wlr_output);
}
fn sendLeaveIterator(surface: *wlr.Surface, _: c_int, _: c_int, wlr_output: *wlr.Output) void {
surface.sendLeave(wlr_output);
}
pub fn close(self: Self) void {
switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.close(),
.xwayland_view => |xwayland_view| xwayland_view.close(),
}
}
pub fn setActivated(self: Self, activated: bool) void {
if (self.foreign_toplevel_handle) |handle| handle.setActivated(activated);
switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.setActivated(activated),
.xwayland_view => |xwayland_view| xwayland_view.setActivated(activated),
}
}
fn setFullscreen(self: *Self, fullscreen: bool) void {
if (self.foreign_toplevel_handle) |handle| handle.setFullscreen(fullscreen);
switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.setFullscreen(fullscreen),
.xwayland_view => |*xwayland_view| {
// TODO(zig): remove this uneeded if statement
// https://github.com/ziglang/zig/issues/13655
if (build_options.xwayland) xwayland_view.setFullscreen(fullscreen);
},
}
}
pub fn setResizing(self: Self, resizing: bool) void {
switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.setResizing(resizing),
.xwayland_view => {},
}
}
/// Iterates over all surfaces, subsurfaces, and popups in the tree
pub inline fn forEachSurface(
self: Self,
comptime T: type,
comptime iterator: fn (surface: *wlr.Surface, sx: c_int, sy: c_int, data: T) void,
user_data: T,
) void {
switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| {
xdg_toplevel.xdg_toplevel.base.forEachSurface(T, iterator, user_data);
},
.xwayland_view => {
assert(build_options.xwayland);
self.surface.?.forEachSurface(T, iterator, user_data);
},
}
}
/// Return the surface at output coordinates ox, oy and set sx, sy to the
/// corresponding surface-relative coordinates, if there is a surface.
pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface {
return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.surfaceAt(ox, oy, sx, sy),
.xwayland_view => |xwayland_view| xwayland_view.surfaceAt(ox, oy, sx, sy),
};
}
/// Return the current title of the view if any.
pub fn getTitle(self: Self) ?[*:0]const u8 {
return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.getTitle(),
.xwayland_view => |xwayland_view| xwayland_view.getTitle(),
};
}
/// Return the current app_id of the view if any.
pub fn getAppId(self: Self) ?[*:0]const u8 {
return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.getAppId(),
.xwayland_view => |xwayland_view| xwayland_view.getAppId(),
};
}
/// Clamp the width/height of the pending state to the constraints of the view
pub fn applyConstraints(self: *Self) void {
const constraints = self.getConstraints();
const box = &self.pending.box;
box.width = math.clamp(box.width, constraints.min_width, constraints.max_width);
box.height = math.clamp(box.height, constraints.min_height, constraints.max_height);
}
/// Return bounds on the dimensions of the view
pub fn getConstraints(self: Self) Constraints {
return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.getConstraints(),
.xwayland_view => |xwayland_view| xwayland_view.getConstraints(),
};
}
/// Modify the pending x/y of the view by the given deltas, clamping to the
/// bounds of the output.
pub fn move(self: *Self, delta_x: i32, delta_y: i32) void {
const border_width = if (self.draw_borders) server.config.border_width else 0;
var output_width: i32 = undefined;
var output_height: i32 = undefined;
self.output.wlr_output.effectiveResolution(&output_width, &output_height);
const max_x = output_width - self.pending.box.width - border_width;
self.pending.box.x += delta_x;
self.pending.box.x = math.max(self.pending.box.x, border_width);
self.pending.box.x = math.min(self.pending.box.x, max_x);
self.pending.box.x = math.max(self.pending.box.x, 0);
const max_y = output_height - self.pending.box.height - border_width;
self.pending.box.y += delta_y;
self.pending.box.y = math.max(self.pending.box.y, border_width);
self.pending.box.y = math.min(self.pending.box.y, max_y);
self.pending.box.y = math.max(self.pending.box.y, 0);
}
/// Find and return the view corresponding to a given surface, if any
pub fn fromWlrSurface(surface: *wlr.Surface) ?*Self {
if (surface.isXdgSurface()) {
const xdg_surface = wlr.XdgSurface.fromWlrSurface(surface) orelse return null;
if (xdg_surface.role == .toplevel) {
return @intToPtr(*Self, xdg_surface.data);
}
}
if (build_options.xwayland and surface.isXWaylandSurface()) {
const xwayland_surface = wlr.XwaylandSurface.fromWlrSurface(surface) orelse return null;
return @intToPtr(?*Self, xwayland_surface.data);
}
if (surface.isSubsurface()) {
if (wlr.Subsurface.fromWlrSurface(surface)) |ss| {
if (ss.parent) |s| {
return fromWlrSurface(s);
}
}
}
return null;
}
pub fn shouldTrackConfigure(self: Self) bool {
// We don't give a damn about frame perfection for xwayland views
if (build_options.xwayland and self.impl == .xwayland_view) return false;
// There are exactly three cases in which we do not track configures
// 1. the view was and remains floating
// 2. the view is changing from float/layout to fullscreen
// 3. the view is changing from fullscreen to float
return !((self.pending.float and self.current.float) or
(self.pending.fullscreen and !self.current.fullscreen) or
(self.pending.float and !self.pending.fullscreen and self.current.fullscreen));
}
/// Called by the impl when the surface is ready to be displayed
pub fn map(self: *Self) !void {
log.debug("view '{?s}' mapped", .{self.getTitle()});
{
assert(self.foreign_toplevel_handle == null);
const handle = try wlr.ForeignToplevelHandleV1.create(server.foreign_toplevel_manager);
self.foreign_toplevel_handle = handle;
handle.events.request_activate.add(&self.foreign_activate);
handle.events.request_fullscreen.add(&self.foreign_fullscreen);
handle.events.request_close.add(&self.foreign_close);
if (self.getTitle()) |s| handle.setTitle(s);
if (self.getAppId()) |s| handle.setAppId(s);
handle.outputEnter(self.output.wlr_output);
}
server.xdg_activation.events.request_activate.add(&self.request_activate);
// Add the view to the stack of its output
const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
self.output.views.attach(node, server.config.attach_mode);
// Inform all seats that the view has been mapped so they can handle focus
var it = server.input_manager.seats.first;
while (it) |seat_node| : (it = seat_node.next) try seat_node.data.handleViewMap(self);
self.sendEnter(self.output);
self.output.sendViewTags();
self.applyPending();
}
/// Called by the impl when the surface will no longer be displayed
pub fn unmap(self: *Self) void {
log.debug("view '{?s}' unmapped", .{self.getTitle()});
if (self.saved_buffers.items.len == 0) self.saveBuffers();
assert(self.surface != null);
self.surface = null;
// Inform all seats that the view has been unmapped so they can handle focus
var it = server.input_manager.seats.first;
while (it) |seat_node| : (it = seat_node.next) seat_node.data.handleViewUnmap(self);
assert(self.foreign_toplevel_handle != null);
self.foreign_activate.link.remove();
self.foreign_fullscreen.link.remove();
self.foreign_close.link.remove();
self.foreign_toplevel_handle.?.destroy();
self.foreign_toplevel_handle = null;
self.request_activate.link.remove();
self.output.sendViewTags();
// Still need to arrange if fullscreened from the layout
if (!self.current.float) self.output.arrangeViews();
server.root.startTransaction();
}
pub fn notifyTitle(self: Self) void {
if (self.foreign_toplevel_handle) |handle| {
if (self.getTitle()) |s| handle.setTitle(s);
}
// Send title to all status listeners attached to a seat which focuses this view
var seat_it = server.input_manager.seats.first;
while (seat_it) |seat_node| : (seat_it = seat_node.next) {
if (seat_node.data.focused == .view and seat_node.data.focused.view == &self) {
var client_it = seat_node.data.status_trackers.first;
while (client_it) |client_node| : (client_it = client_node.next) {
client_node.data.sendFocusedView();
}
}
}
}
pub fn notifyAppId(self: Self) void {
if (self.foreign_toplevel_handle) |handle| {
if (self.getAppId()) |s| handle.setAppId(s);
}
}
/// Only honors the request if the view is already visible on the seat's
/// currently focused output. TODO: consider allowing this request to switch
/// output/tag focus.
fn handleForeignActivate(
listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated),
event: *wlr.ForeignToplevelHandleV1.event.Activated,
) void {
const self = @fieldParentPtr(Self, "foreign_activate", listener);
const seat = @intToPtr(*Seat, event.seat.data);
seat.focus(self);
server.root.startTransaction();
}
fn handleForeignFullscreen(
listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen),
event: *wlr.ForeignToplevelHandleV1.event.Fullscreen,
) void {
const self = @fieldParentPtr(Self, "foreign_fullscreen", listener);
self.pending.fullscreen = event.fullscreen;
self.applyPending();
}
fn handleForeignClose(
listener: *wl.Listener(*wlr.ForeignToplevelHandleV1),
_: *wlr.ForeignToplevelHandleV1,
) void {
const self = @fieldParentPtr(Self, "foreign_close", listener);
self.close();
}
fn handleRequestActivate(
_: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate),
event: *wlr.XdgActivationV1.event.RequestActivate,
) void {
if (fromWlrSurface(event.surface)) |view| {
if (view.current.focus == 0) {
view.pending.urgent = true;
server.root.startTransaction();
}
}
}