river/river/XwaylandView.zig
Isaac Freund 615beab2e6
Seat: rework Xwayland Override Redirect focus
Instead of stashing the active view and setting Seat.focused to the
Xwayland OR surface when a child OR surface of a currently focused
Xwayland view is given keyboard focus, keep Seat.focused set to the
Xwayland view.

Such Override Redirect surfaces are commonly used for drop down menus
and the like, and river should behave as if the parent Xwayland view
still has focus.

This ensures that the riverctl focus-view next/prev commands continue to
work as expected while a popup is open, the correct focused view title
will be sent over river status, etc.

It's also cleaner to centralize this logic in XwaylandOverrideRedirect
and keep it out of Seat.zig.
2023-01-12 14:44:55 +01:00

339 lines
13 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 std = @import("std");
const assert = std.debug.assert;
const math = std.math;
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 View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
const XdgPopup = @import("XdgPopup.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
const log = std.log.scoped(.xwayland);
/// The view this xwayland view implements
view: *View,
/// The corresponding wlroots object
xwayland_surface: *wlr.XwaylandSurface,
/// The wlroots Xwayland implementation overwrites xwayland_surface.fullscreen
/// immediately when the client requests it, so we track this state here to be
/// able to match the XdgToplevel API.
last_set_fullscreen_state: bool = false,
// Listeners that are always active over the view's lifetime
destroy: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleDestroy),
map: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleMap),
unmap: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleUnmap),
request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) =
wl.Listener(*wlr.XwaylandSurface.event.Configure).init(handleRequestConfigure),
set_override_redirect: wl.Listener(*wlr.XwaylandSurface) =
wl.Listener(*wlr.XwaylandSurface).init(handleSetOverrideRedirect),
// Listeners that are only active while the view is mapped
commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
set_title: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleSetTitle),
set_class: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleSetClass),
request_fullscreen: wl.Listener(*wlr.XwaylandSurface) =
wl.Listener(*wlr.XwaylandSurface).init(handleRequestFullscreen),
request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) =
wl.Listener(*wlr.XwaylandSurface.event.Minimize).init(handleRequestMinimize),
/// The View will add itself to the output's view stack on map
pub fn create(output: *Output, xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!*Self {
const node = try util.gpa.create(ViewStack(View).Node);
const view = &node.view;
view.init(output, .{ .xwayland_view = .{
.view = view,
.xwayland_surface = xwayland_surface,
} });
const self = &node.view.impl.xwayland_view;
xwayland_surface.data = @ptrToInt(self);
// Add listeners that are active over the view's entire lifetime
xwayland_surface.events.destroy.add(&self.destroy);
xwayland_surface.events.map.add(&self.map);
xwayland_surface.events.unmap.add(&self.unmap);
xwayland_surface.events.request_configure.add(&self.request_configure);
xwayland_surface.events.set_override_redirect.add(&self.set_override_redirect);
return self;
}
pub fn needsConfigure(self: Self) bool {
const output = self.view.output;
var output_box: wlr.Box = undefined;
server.root.output_layout.getBox(output.wlr_output, &output_box);
return self.xwayland_surface.x != self.view.pending.box.x + output_box.x or
self.xwayland_surface.y != self.view.pending.box.y + output_box.y or
self.xwayland_surface.width != self.view.pending.box.width or
self.xwayland_surface.height != self.view.pending.box.height;
}
/// Apply pending state. Note: we don't set View.serial as
/// shouldTrackConfigure() is always false for xwayland views.
pub fn configure(self: Self) void {
const output = self.view.output;
var output_box: wlr.Box = undefined;
server.root.output_layout.getBox(output.wlr_output, &output_box);
const state = &self.view.pending;
self.xwayland_surface.configure(
@intCast(i16, state.box.x + output_box.x),
@intCast(i16, state.box.y + output_box.y),
@intCast(u16, state.box.width),
@intCast(u16, state.box.height),
);
}
pub fn lastSetFullscreenState(self: Self) bool {
return self.last_set_fullscreen_state;
}
/// Close the view. This will lead to the unmap and destroy events being sent
pub fn close(self: Self) void {
self.xwayland_surface.close();
}
pub fn setActivated(self: Self, activated: bool) void {
// See comment on handleRequestMinimize() for details
if (activated and self.xwayland_surface.minimized) {
self.xwayland_surface.setMinimized(false);
}
self.xwayland_surface.activate(activated);
self.xwayland_surface.restack(null, .above);
}
pub fn setFullscreen(self: *Self, fullscreen: bool) void {
self.last_set_fullscreen_state = fullscreen;
self.xwayland_surface.setFullscreen(fullscreen);
}
/// 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 self.xwayland_surface.surface.?.surfaceAt(
ox - @intToFloat(f64, self.view.current.box.x),
oy - @intToFloat(f64, self.view.current.box.y),
sx,
sy,
);
}
/// Get the current title of the xwayland surface if any.
pub fn getTitle(self: Self) ?[*:0]const u8 {
return self.xwayland_surface.title;
}
/// X11 clients don't have an app_id but the class serves a similar role.
/// Get the current class of the xwayland surface if any.
pub fn getAppId(self: Self) ?[*:0]const u8 {
return self.xwayland_surface.class;
}
/// Return bounds on the dimensions of the view
pub fn getConstraints(self: Self) View.Constraints {
const hints = self.xwayland_surface.size_hints orelse return .{
.min_width = 1,
.min_height = 1,
.max_width = math.maxInt(u31),
.max_height = math.maxInt(u31),
};
return .{
.min_width = @intCast(u31, math.max(hints.min_width, 1)),
.min_height = @intCast(u31, math.max(hints.min_height, 1)),
.max_width = if (hints.max_width > 0) @intCast(u31, hints.max_width) else math.maxInt(u31),
.max_height = if (hints.max_height > 0) @intCast(u31, hints.max_height) else math.maxInt(u31),
};
}
/// Called when the xwayland surface is destroyed
fn handleDestroy(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void {
const self = @fieldParentPtr(Self, "destroy", listener);
// Remove listeners that are active for the entire lifetime of the view
self.destroy.link.remove();
self.map.link.remove();
self.unmap.link.remove();
self.request_configure.link.remove();
self.set_override_redirect.link.remove();
self.view.destroy();
}
/// Called when the xwayland surface is mapped, or ready to display on-screen.
pub fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void {
const self = @fieldParentPtr(Self, "map", listener);
const view = self.view;
// Add listeners that are only active while mapped
const surface = xwayland_surface.surface.?;
surface.events.commit.add(&self.commit);
xwayland_surface.events.set_title.add(&self.set_title);
xwayland_surface.events.set_class.add(&self.set_class);
xwayland_surface.events.request_fullscreen.add(&self.request_fullscreen);
xwayland_surface.events.request_minimize.add(&self.request_minimize);
view.surface = surface;
self.view.surface_box = .{
.x = 0,
.y = 0,
.width = surface.current.width,
.height = surface.current.height,
};
// Use the view's "natural" size centered on the output as the default
// floating dimensions
view.float_box = .{
.x = @divTrunc(math.max(0, view.output.usable_box.width - self.xwayland_surface.width), 2),
.y = @divTrunc(math.max(0, view.output.usable_box.height - self.xwayland_surface.height), 2),
.width = self.xwayland_surface.width,
.height = self.xwayland_surface.height,
};
const has_fixed_size = if (self.xwayland_surface.size_hints) |size_hints|
size_hints.min_width != 0 and size_hints.min_height != 0 and
(size_hints.min_width == size_hints.max_width or size_hints.min_height == size_hints.max_height)
else
false;
if (self.xwayland_surface.parent != null or has_fixed_size) {
// If the toplevel has a parent or has a fixed size make it float
view.pending.float = true;
} else if (server.config.shouldFloat(view)) {
view.pending.float = true;
}
view.pending.fullscreen = xwayland_surface.fullscreen;
view.map() catch {
log.err("out of memory", .{});
surface.resource.getClient().postNoMemory();
};
}
/// Called when the surface is unmapped and will no longer be displayed.
fn handleUnmap(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void {
const self = @fieldParentPtr(Self, "unmap", listener);
// Remove listeners that are only active while mapped
self.commit.link.remove();
self.set_title.link.remove();
self.set_class.link.remove();
self.request_fullscreen.link.remove();
self.request_minimize.link.remove();
self.view.unmap();
}
fn handleRequestConfigure(
listener: *wl.Listener(*wlr.XwaylandSurface.event.Configure),
event: *wlr.XwaylandSurface.event.Configure,
) void {
const self = @fieldParentPtr(Self, "request_configure", listener);
// If unmapped, let the client do whatever it wants
if (self.view.surface == null) {
self.xwayland_surface.configure(event.x, event.y, event.width, event.height);
return;
}
// Allow xwayland views to set their own dimensions (but not position) if floating
if (self.view.pending.float) {
self.view.pending.box.width = event.width;
self.view.pending.box.height = event.height;
}
self.configure();
}
fn handleSetOverrideRedirect(
listener: *wl.Listener(*wlr.XwaylandSurface),
xwayland_surface: *wlr.XwaylandSurface,
) void {
const self = @fieldParentPtr(Self, "set_override_redirect", listener);
log.debug("xwayland surface set override redirect", .{});
assert(xwayland_surface.override_redirect);
if (xwayland_surface.mapped) handleUnmap(&self.unmap, xwayland_surface);
handleDestroy(&self.destroy, xwayland_surface);
const override_redirect = XwaylandOverrideRedirect.create(xwayland_surface) catch {
log.err("out of memory", .{});
return;
};
if (xwayland_surface.mapped) {
XwaylandOverrideRedirect.handleMap(&override_redirect.map, xwayland_surface);
}
}
fn handleCommit(listener: *wl.Listener(*wlr.Surface), surface: *wlr.Surface) void {
const self = @fieldParentPtr(Self, "commit", listener);
self.view.output.damage.?.addWhole();
self.view.surface_box = .{
.x = 0,
.y = 0,
.width = surface.current.width,
.height = surface.current.height,
};
}
fn handleSetTitle(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void {
const self = @fieldParentPtr(Self, "set_title", listener);
self.view.notifyTitle();
}
fn handleSetClass(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void {
const self = @fieldParentPtr(Self, "set_class", listener);
self.view.notifyAppId();
}
fn handleRequestFullscreen(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void {
const self = @fieldParentPtr(Self, "request_fullscreen", listener);
if (self.view.pending.fullscreen != xwayland_surface.fullscreen) {
self.view.pending.fullscreen = xwayland_surface.fullscreen;
self.view.applyPending();
}
}
/// Some X11 clients will minimize themselves regardless of how we respond.
/// Therefore to ensure they don't get stuck in this minimized state we tell
/// them their request has been honored without actually doing anything and
/// unminimize them if they gain focus while minimized.
fn handleRequestMinimize(
listener: *wl.Listener(*wlr.XwaylandSurface.event.Minimize),
event: *wlr.XwaylandSurface.event.Minimize,
) void {
const self = @fieldParentPtr(Self, "request_minimize", listener);
self.xwayland_surface.setMinimized(event.minimize);
}