foreign-toplevel-management: basic implementation

We do no yet set the parent of toplevels. We also only honor activate
requests if the target view is already visible on the focused output.
This commit is contained in:
Isaac Freund 2020-12-24 01:59:30 +01:00
parent 386316bdbd
commit 760c88b094
No known key found for this signature in database
GPG key ID: 86DED400DDFD7A11
8 changed files with 142 additions and 26 deletions

2
deps/zig-wayland vendored

@ -1 +1 @@
Subproject commit 52326e7ee09d7acb6b55855f7a697af083ae973a Subproject commit d693b3704ee73762c71a68d634ef1a538c3307bd

2
deps/zig-wlroots vendored

@ -1 +1 @@
Subproject commit 16d9039b5c345b2cc26118032261df9782e24946 Subproject commit 10ddf02b8aff37dabd07cee8695366917527676e

View file

@ -65,6 +65,9 @@ pub fn sendOutput(self: Self, state: enum { focused, unfocused }) void {
} }
pub fn sendFocusedView(self: Self) void { pub fn sendFocusedView(self: Self) void {
const title: [*:0]const u8 = if (self.seat.focused == .view) self.seat.focused.view.getTitle() else ""; const title: [*:0]const u8 = if (self.seat.focused == .view)
self.seat.focused.view.getTitle() orelse ""
else
"";
self.seat_status.sendFocusedView(title); self.seat_status.sendFocusedView(title);
} }

View file

@ -56,6 +56,8 @@ new_layer_surface: wl.Listener(*wlr.LayerSurfaceV1),
xwayland: if (build_options.xwayland) *wlr.Xwayland else void, xwayland: if (build_options.xwayland) *wlr.Xwayland else void,
new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void, new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void,
foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
decoration_manager: DecorationManager, decoration_manager: DecorationManager,
input_manager: InputManager, input_manager: InputManager,
output_manager: OutputManager, output_manager: OutputManager,
@ -104,6 +106,8 @@ pub fn init(self: *Self) !void {
self.xwayland.events.new_surface.add(&self.new_xwayland_surface); self.xwayland.events.new_surface.add(&self.new_xwayland_surface);
} }
self.foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(self.wl_server);
_ = try wlr.PrimarySelectionDeviceManagerV1.create(self.wl_server); _ = try wlr.PrimarySelectionDeviceManagerV1.create(self.wl_server);
self.config = try Config.init(); self.config = try Config.init();
@ -135,8 +139,8 @@ pub fn deinit(self: *Self) void {
self.root.deinit(); self.root.deinit();
self.wl_server.destroy();
self.noop_backend.destroy(); self.noop_backend.destroy();
self.wl_server.destroy();
self.input_manager.deinit(); self.input_manager.deinit();
self.config.deinit(); self.config.deinit();

View file

@ -29,6 +29,7 @@ const util = @import("util.zig");
const Box = @import("Box.zig"); const Box = @import("Box.zig");
const Output = @import("Output.zig"); const Output = @import("Output.zig");
const Root = @import("Root.zig"); const Root = @import("Root.zig");
const Seat = @import("Seat.zig");
const ViewStack = @import("view_stack.zig").ViewStack; const ViewStack = @import("view_stack.zig").ViewStack;
const XdgToplevel = @import("XdgToplevel.zig"); const XdgToplevel = @import("XdgToplevel.zig");
const XwaylandView = if (build_options.xwayland) @import("XwaylandView.zig") else @import("VoidView.zig"); const XwaylandView = if (build_options.xwayland) @import("XwaylandView.zig") else @import("VoidView.zig");
@ -84,8 +85,8 @@ impl: Impl = undefined,
/// The output this view is currently associated with /// The output this view is currently associated with
output: *Output, output: *Output,
/// This is from the point where the view is mapped until the surface /// This is non-null from the point where the view is mapped until the
/// is destroyed by wlroots. /// surface is destroyed by wlroots.
surface: ?*wlr.Surface = null, surface: ?*wlr.Surface = null,
/// This View struct outlasts the wlroots object it wraps. This bool is set to /// This View struct outlasts the wlroots object it wraps. This bool is set to
@ -122,6 +123,12 @@ opacity_timer: ?*wl.EventSource = null,
draw_borders: bool = true, 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) = undefined,
foreign_fullscreen: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen) = undefined,
foreign_close: wl.Listener(*wlr.ForeignToplevelHandleV1) = undefined,
pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void { pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void {
self.* = .{ self.* = .{
.output = output, .output = output,
@ -150,10 +157,19 @@ pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void {
pub fn destroy(self: *Self) void { pub fn destroy(self: *Self) void {
self.dropSavedBuffers(); self.dropSavedBuffers();
self.saved_buffers.deinit(); 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) { switch (self.impl) {
.xdg_toplevel => |*xdg_toplevel| xdg_toplevel.deinit(), .xdg_toplevel => |*xdg_toplevel| xdg_toplevel.deinit(),
.xwayland_view => |*xwayland_view| xwayland_view.deinit(), .xwayland_view => |*xwayland_view| xwayland_view.deinit(),
} }
const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
self.output.views.remove(node); self.output.views.remove(node);
util.gpa.destroy(node); util.gpa.destroy(node);
@ -220,6 +236,10 @@ pub fn needsConfigure(self: Self) bool {
} }
pub fn configure(self: Self) void { pub fn configure(self: Self) void {
if (self.foreign_toplevel_handle) |handle| {
handle.setActivated(self.pending.focus != 0);
handle.setFullscreen(self.pending.fullscreen);
}
switch (self.impl) { switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.configure(), .xdg_toplevel => |xdg_toplevel| xdg_toplevel.configure(),
.xwayland_view => |xwayland_view| xwayland_view.configure(), .xwayland_view => |xwayland_view| xwayland_view.configure(),
@ -293,6 +313,9 @@ pub fn sendToOutput(self: *Self, destination_output: *Output) void {
self.surface.?.sendLeave(self.output.wlr_output); self.surface.?.sendLeave(self.output.wlr_output);
self.surface.?.sendEnter(destination_output.wlr_output); self.surface.?.sendEnter(destination_output.wlr_output);
self.foreign_toplevel_handle.?.outputLeave(self.output.wlr_output);
self.foreign_toplevel_handle.?.outputEnter(destination_output.wlr_output);
self.output = destination_output; self.output = destination_output;
} }
@ -324,14 +347,23 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface
}; };
} }
/// Return the current title of the view. May be an empty string. /// Return the current title of the view if any.
pub fn getTitle(self: Self) [*:0]const u8 { pub fn getTitle(self: Self) ?[*:0]const u8 {
return switch (self.impl) { return switch (self.impl) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.getTitle(), .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getTitle(),
.xwayland_view => |xwayland_view| xwayland_view.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(),
// X11 clients don't have an app_id but the class serves a similar role
.xwayland_view => |xwayland_view| xwayland_view.getClass(),
};
}
/// Clamp the width/height of the pending state to the constraints of the view /// Clamp the width/height of the pending state to the constraints of the view
pub fn applyConstraints(self: *Self) void { pub fn applyConstraints(self: *Self) void {
const constraints = self.getConstraints(); const constraints = self.getConstraints();
@ -383,6 +415,28 @@ pub fn map(self: *Self) void {
log.debug(.server, "view '{}' mapped", .{self.getTitle()}); log.debug(.server, "view '{}' mapped", .{self.getTitle()});
if (self.foreign_toplevel_handle == null) {
self.foreign_toplevel_handle = wlr.ForeignToplevelHandleV1.create(
root.server.foreign_toplevel_manager,
) catch {
log.crit(.server, "out of memory", .{});
self.surface.?.resource.getClient().postNoMemory();
return;
};
self.foreign_activate.setNotify(handleForeignActivate);
self.foreign_toplevel_handle.?.events.request_activate.add(&self.foreign_activate);
self.foreign_fullscreen.setNotify(handleForeignFullscreen);
self.foreign_toplevel_handle.?.events.request_fullscreen.add(&self.foreign_fullscreen);
self.foreign_close.setNotify(handleForeignClose);
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);
}
// Add the view to the stack of its output // Add the view to the stack of its output
const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
self.output.views.attach(node, self.output.attach_mode); self.output.views.attach(node, self.output.attach_mode);
@ -428,6 +482,28 @@ pub fn unmap(self: *Self) void {
root.startTransaction(); 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 = self.output.root.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);
}
}
/// Change the opacity of a view by config.view_opacity_delta. /// Change the opacity of a view by config.view_opacity_delta.
/// If the target opacity was reached, return true. /// If the target opacity was reached, return true.
fn incrementOpacity(self: *Self) bool { fn incrementOpacity(self: *Self) bool {
@ -491,3 +567,33 @@ pub fn commitOpacityTransition(self: *Self) void {
self.attachOpacityTimer(); self.attachOpacityTimer();
} }
} }
/// 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);
self.output.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();
}

View file

@ -52,7 +52,11 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface
unreachable; unreachable;
} }
pub fn getTitle(self: Self) [*:0]const u8 { pub fn getTitle(self: Self) ?[*:0]const u8 {
unreachable;
}
pub fn getClass(self: Self) ?[*:0]const u8 {
unreachable; unreachable;
} }

View file

@ -123,9 +123,14 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface
); );
} }
/// Return the current title of the toplevel. May be an empty string. /// Return the current title of the toplevel if any.
pub fn getTitle(self: Self) [*:0]const u8 { pub fn getTitle(self: Self) ?[*:0]const u8 {
return self.xdg_surface.role_data.toplevel.title orelse "NULL"; return self.xdg_surface.role_data.toplevel.title;
}
/// Return the current app_id of the toplevel if any .
pub fn getAppId(self: Self) ?[*:0]const u8 {
return self.xdg_surface.role_data.toplevel.app_id;
} }
/// Return bounds on the dimensions of the toplevel. /// Return bounds on the dimensions of the toplevel.
@ -310,15 +315,4 @@ fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), ev
/// Called when the client sets / updates its title /// Called when the client sets / updates its title
fn handleSetTitle(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void { fn handleSetTitle(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void {
const self = @fieldParentPtr(Self, "set_title", listener); const self = @fieldParentPtr(Self, "set_title", listener);
// Send title to all status listeners attached to a seat which focuses this view
var seat_it = self.view.output.root.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.view) {
var client_it = seat_node.data.status_trackers.first;
while (client_it) |client_node| : (client_it = client_node.next) {
client_node.data.sendFocusedView();
}
}
}
} }

View file

@ -121,9 +121,14 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface
); );
} }
/// Get the current title of the xwayland surface. May be an empty string /// Get the current title of the xwayland surface if any.
pub fn getTitle(self: Self) [*:0]const u8 { pub fn getTitle(self: Self) ?[*:0]const u8 {
return self.xwayland_surface.title orelse ""; return self.xwayland_surface.title;
}
/// Get the current class of the xwayland surface if any.
pub fn getClass(self: Self) ?[*:0]const u8 {
return self.xwayland_surface.class;
} }
/// Return bounds on the dimensions of the view /// Return bounds on the dimensions of the view