96e1082156
This code is complex and increases maintenance burden but doesn't add any functionality, only eye-candy. Futhermore, neither I nor any of the core contributors use it. There may be a place in river for such eye-candy down the line, in which case this code could be revived. Currently river is early enough in its development that our focus should be on core functionality instead.
543 lines
19 KiB
Zig
543 lines
19 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, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const Self = @This();
|
|
|
|
const build_options = @import("build_options");
|
|
const std = @import("std");
|
|
const 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 Box = @import("Box.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 = if (build_options.xwayland) @import("XwaylandView.zig") else @import("VoidView.zig");
|
|
|
|
const log = std.log.scoped(.view);
|
|
|
|
pub const Constraints = struct {
|
|
min_width: u32,
|
|
max_width: u32,
|
|
min_height: u32,
|
|
max_height: u32,
|
|
};
|
|
|
|
// Minimum width/height for surfaces.
|
|
// This is needed, because external layouts and large padding and border sizes
|
|
// may cause surfaces so small, that bugs in client applications are encountered,
|
|
// or even surfaces of zero or negative size,which are a protocol error and would
|
|
// likely cause river to crash. The value is totally arbitrary and low enough,
|
|
// that it should never be encountered during normal usage.
|
|
pub const min_size = 50;
|
|
|
|
const Impl = union(enum) {
|
|
xdg_toplevel: XdgToplevel,
|
|
xwayland_view: XwaylandView,
|
|
};
|
|
|
|
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: Box = 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,
|
|
};
|
|
|
|
const SavedBuffer = struct {
|
|
client_buffer: *wlr.ClientBuffer,
|
|
box: Box,
|
|
transform: wl.Output.Transform,
|
|
};
|
|
|
|
/// The implementation of this view
|
|
impl: Impl = undefined,
|
|
|
|
/// The output this view is currently associated with
|
|
output: *Output,
|
|
|
|
/// This is non-null from the point where the view is mapped until the
|
|
/// surface is destroyed by wlroots.
|
|
surface: ?*wlr.Surface = null,
|
|
|
|
/// This View struct outlasts the wlroots object it wraps. This bool is set to
|
|
/// true when the backing wlr.XdgToplevel or equivalent has been destroyed.
|
|
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: Box = undefined,
|
|
|
|
/// The geometry the view's surface had when the transaction started and
|
|
/// buffers were saved.
|
|
saved_surface_box: Box = undefined,
|
|
|
|
/// These are what we render while a transaction is in progress
|
|
saved_buffers: std.ArrayList(SavedBuffer),
|
|
|
|
/// The floating dimensions the view, saved so that they can be restored if the
|
|
/// view returns to floating mode.
|
|
float_box: Box = undefined,
|
|
|
|
/// While a view is in fullscreen, it is still arranged if a layout is active but
|
|
/// the resulting dimensions are stored here instead of being applied to the view's
|
|
/// state. This allows us to avoid an arrange when the view returns from fullscreen
|
|
/// and for more intuitive behavior if there is no active layout for the output.
|
|
post_fullscreen_box: Box = undefined,
|
|
|
|
draw_borders: bool = true,
|
|
|
|
/// This is created when the view is mapped and destroyed with the view
|
|
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),
|
|
|
|
pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void {
|
|
self.* = .{
|
|
.output = output,
|
|
.current = .{ .tags = tags },
|
|
.pending = .{ .tags = tags },
|
|
.saved_buffers = std.ArrayList(SavedBuffer).init(util.gpa),
|
|
};
|
|
|
|
if (@TypeOf(surface) == *wlr.XdgSurface) {
|
|
self.impl = .{ .xdg_toplevel = undefined };
|
|
self.impl.xdg_toplevel.init(self, surface);
|
|
} else if (build_options.xwayland and @TypeOf(surface) == *wlr.XwaylandSurface) {
|
|
self.impl = .{ .xwayland_view = undefined };
|
|
self.impl.xwayland_view.init(self, surface);
|
|
} else unreachable;
|
|
}
|
|
|
|
/// Deinit the view, remove it from the view stack and free the memory.
|
|
pub fn destroy(self: *Self) void {
|
|
self.dropSavedBuffers();
|
|
self.saved_buffers.deinit();
|
|
|
|
if (self.foreign_toplevel_handle) |handle| {
|
|
self.foreign_activate.link.remove();
|
|
self.foreign_fullscreen.link.remove();
|
|
self.foreign_close.link.remove();
|
|
handle.destroy();
|
|
}
|
|
|
|
switch (self.impl) {
|
|
.xdg_toplevel => |*xdg_toplevel| xdg_toplevel.deinit(),
|
|
.xwayland_view => |*xwayland_view| xwayland_view.deinit(),
|
|
}
|
|
|
|
const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
|
|
self.output.views.remove(node);
|
|
util.gpa.destroy(node);
|
|
}
|
|
|
|
/// Handle changes to pending state and start a transaction to apply them
|
|
pub fn applyPending(self: *Self) void {
|
|
var arrange_output = false;
|
|
|
|
if (self.current.tags != self.pending.tags)
|
|
arrange_output = true;
|
|
|
|
// If switching from float -> layout or layout -> float arrange the output
|
|
// to get assigned a new size or fill the hole in the layout left behind
|
|
if (self.current.float != self.pending.float)
|
|
arrange_output = true;
|
|
|
|
// If switching from float to non-float, save the dimensions
|
|
if (self.current.float and !self.pending.float)
|
|
self.float_box = self.current.box;
|
|
|
|
// If switching from non-float to float, apply the saved float dimensions
|
|
if (!self.current.float and self.pending.float)
|
|
self.pending.box = self.float_box;
|
|
|
|
// If switching to fullscreen set the dimensions to the full area of the output
|
|
// and turn the view fully opaque
|
|
if (!self.current.fullscreen and self.pending.fullscreen) {
|
|
self.setFullscreen(true);
|
|
self.post_fullscreen_box = self.current.box;
|
|
|
|
const layout_box = server.root.output_layout.getBox(self.output.wlr_output).?;
|
|
self.pending.box = .{
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = @intCast(u32, layout_box.width),
|
|
.height = @intCast(u32, layout_box.height),
|
|
};
|
|
}
|
|
|
|
if (self.current.fullscreen and !self.pending.fullscreen) {
|
|
self.setFullscreen(false);
|
|
self.pending.box = self.post_fullscreen_box;
|
|
}
|
|
|
|
if (arrange_output) 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| xwayland_view.configure(),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
std.debug.assert(self.saved_buffers.items.len == 0);
|
|
self.saved_surface_box = self.surface_box;
|
|
self.surface.?.forEachSurface(*std.ArrayList(SavedBuffer), saveBuffersIterator, &self.saved_buffers);
|
|
}
|
|
|
|
fn saveBuffersIterator(
|
|
surface: *wlr.Surface,
|
|
surface_x: c_int,
|
|
surface_y: c_int,
|
|
saved_buffers: *std.ArrayList(SavedBuffer),
|
|
) callconv(.C) void {
|
|
if (surface.buffer) |buffer| {
|
|
saved_buffers.append(.{
|
|
.client_buffer = buffer,
|
|
.box = Box{
|
|
.x = surface_x,
|
|
.y = surface_y,
|
|
.width = @intCast(u32, surface.current.width),
|
|
.height = @intCast(u32, surface.current.height),
|
|
},
|
|
.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.surface) |surface| {
|
|
surface.sendLeave(self.output.wlr_output);
|
|
surface.sendEnter(destination_output.wlr_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;
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|
|
|
|
pub 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| 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 => {},
|
|
}
|
|
}
|
|
|
|
pub inline fn forEachPopupSurface(
|
|
self: Self,
|
|
comptime T: type,
|
|
iterator: fn (surface: *wlr.Surface, sx: c_int, sy: c_int, data: T) callconv(.C) void,
|
|
user_data: T,
|
|
) void {
|
|
switch (self.impl) {
|
|
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.forEachPopupSurface(T, iterator, user_data),
|
|
.xwayland_view => {},
|
|
}
|
|
}
|
|
|
|
/// 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) @intCast(i32, server.config.border_width) else 0;
|
|
const output_resolution = self.output.getEffectiveResolution();
|
|
|
|
const max_x = @intCast(i32, output_resolution.width) - @intCast(i32, 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 = @intCast(i32, output_resolution.height) - @intCast(i32, 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);
|
|
if (xdg_surface.role == .toplevel) {
|
|
return @intToPtr(*Self, xdg_surface.data);
|
|
}
|
|
}
|
|
if (build_options.xwayland) {
|
|
if (surface.isXWaylandSurface()) {
|
|
const xwayland_surface = wlr.XwaylandSurface.fromWlrSurface(surface);
|
|
return @intToPtr(*Self, xwayland_surface.data);
|
|
}
|
|
}
|
|
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()});
|
|
|
|
if (self.foreign_toplevel_handle == null) {
|
|
self.foreign_toplevel_handle = wlr.ForeignToplevelHandleV1.create(
|
|
server.foreign_toplevel_manager,
|
|
) catch {
|
|
log.crit("out of memory", .{});
|
|
self.surface.?.resource.getClient().postNoMemory();
|
|
return;
|
|
};
|
|
|
|
self.foreign_toplevel_handle.?.events.request_activate.add(&self.foreign_activate);
|
|
self.foreign_toplevel_handle.?.events.request_fullscreen.add(&self.foreign_fullscreen);
|
|
self.foreign_toplevel_handle.?.events.request_close.add(&self.foreign_close);
|
|
|
|
if (self.getTitle()) |s| self.foreign_toplevel_handle.?.setTitle(s);
|
|
if (self.getAppId()) |s| self.foreign_toplevel_handle.?.setAppId(s);
|
|
|
|
self.foreign_toplevel_handle.?.outputEnter(self.output.wlr_output);
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Focus the new view, assuming the seat is focusing the proper output
|
|
// and there isn't something else like a fullscreen view grabbing focus.
|
|
var it = server.input_manager.seats.first;
|
|
while (it) |seat_node| : (it = seat_node.next) seat_node.data.focus(self);
|
|
|
|
self.surface.?.sendEnter(self.output.wlr_output);
|
|
|
|
self.output.sendViewTags();
|
|
|
|
if (!self.current.float) self.output.arrangeViews();
|
|
|
|
server.root.startTransaction();
|
|
}
|
|
|
|
/// 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()});
|
|
|
|
assert(!self.destroying);
|
|
self.destroying = true;
|
|
|
|
if (self.saved_buffers.items.len == 0) self.saveBuffers();
|
|
|
|
// Inform all seats that the view has been unmapped so they can handle focus
|
|
var it = server.input_manager.seats.first;
|
|
while (it) |node| : (it = node.next) {
|
|
const seat = &node.data;
|
|
seat.handleViewUnmap(self);
|
|
}
|
|
|
|
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),
|
|
event: *wlr.ForeignToplevelHandleV1,
|
|
) void {
|
|
const self = @fieldParentPtr(Self, "foreign_close", listener);
|
|
self.close();
|
|
}
|