mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-26 09:13:56 +00:00
gtk/wayland: refactor global handling
The way we originally handled globals gradually escalated into an unholy mess of ad-hoc helper functions and special-case handlers, which proved to be hard to scale. Using a type-erased EnumMap like this makes everything *far* easier to work and reason with, I think. Also nuked the `xdg_wm_dialog_v1` hack that was necessary to prevent old versions of gtk4-layer-shell crashing. If by the time of 1.4's release people are still using those versions, it's on them.
This commit is contained in:
@@ -350,7 +350,7 @@ pub const Application = extern struct {
|
|||||||
log.warn("error initializing windowing protocol err={}", .{err});
|
log.warn("error initializing windowing protocol err={}", .{err});
|
||||||
break :wp .{ .none = .{} };
|
break :wp .{ .none = .{} };
|
||||||
};
|
};
|
||||||
errdefer wp.deinit(alloc);
|
errdefer wp.deinit();
|
||||||
log.debug("windowing protocol={s}", .{@tagName(wp)});
|
log.debug("windowing protocol={s}", .{@tagName(wp)});
|
||||||
|
|
||||||
// Create our GTK Application which encapsulates our process.
|
// Create our GTK Application which encapsulates our process.
|
||||||
@@ -431,7 +431,7 @@ pub const Application = extern struct {
|
|||||||
const alloc = self.allocator();
|
const alloc = self.allocator();
|
||||||
const priv: *Private = self.private();
|
const priv: *Private = self.private();
|
||||||
priv.config.unref();
|
priv.config.unref();
|
||||||
priv.winproto.deinit(alloc);
|
priv.winproto.deinit();
|
||||||
priv.global_shortcuts.unref();
|
priv.global_shortcuts.unref();
|
||||||
if (priv.saved_language) |language| alloc.free(language);
|
if (priv.saved_language) |language| alloc.free(language);
|
||||||
if (gdk.Display.getDefault()) |display| {
|
if (gdk.Display.getDefault()) |display| {
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ pub const App = union(Protocol) {
|
|||||||
return .{ .none = .{} };
|
return .{ .none = .{} };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
pub fn deinit(self: *App) void {
|
||||||
switch (self.*) {
|
switch (self.*) {
|
||||||
inline else => |*v| v.deinit(alloc),
|
inline else => |*v| v.deinit(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ pub const App = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
pub fn deinit(self: *App) void {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = alloc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eventMods(
|
pub fn eventMods(
|
||||||
|
|||||||
@@ -7,66 +7,24 @@ const gdk_wayland = @import("gdk_wayland");
|
|||||||
const gobject = @import("gobject");
|
const gobject = @import("gobject");
|
||||||
const gtk = @import("gtk");
|
const gtk = @import("gtk");
|
||||||
const layer_shell = @import("gtk4-layer-shell");
|
const layer_shell = @import("gtk4-layer-shell");
|
||||||
|
|
||||||
const wayland = @import("wayland");
|
const wayland = @import("wayland");
|
||||||
|
|
||||||
const Config = @import("../../../config.zig").Config;
|
|
||||||
const input = @import("../../../input.zig");
|
|
||||||
const ApprtWindow = @import("../class/window.zig").Window;
|
|
||||||
|
|
||||||
const wl = wayland.client.wl;
|
const wl = wayland.client.wl;
|
||||||
const kde = wayland.client.kde;
|
const kde = wayland.client.kde;
|
||||||
const org = wayland.client.org;
|
const org = wayland.client.org;
|
||||||
const xdg = wayland.client.xdg;
|
const xdg = wayland.client.xdg;
|
||||||
|
|
||||||
|
const Config = @import("../../../config.zig").Config;
|
||||||
|
const Globals = @import("wayland/Globals.zig");
|
||||||
|
const input = @import("../../../input.zig");
|
||||||
|
const ApprtWindow = @import("../class/window.zig").Window;
|
||||||
|
|
||||||
const log = std.log.scoped(.winproto_wayland);
|
const log = std.log.scoped(.winproto_wayland);
|
||||||
|
|
||||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||||
pub const App = struct {
|
pub const App = struct {
|
||||||
display: *wl.Display,
|
display: *wl.Display,
|
||||||
context: *Context,
|
globals: *Globals,
|
||||||
|
|
||||||
const Context = struct {
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
|
||||||
|
|
||||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
|
||||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
|
||||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
|
||||||
kde_decoration_manager_global_name: ?u32 = null,
|
|
||||||
|
|
||||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
|
||||||
|
|
||||||
kde_output_order: ?*kde.OutputOrderV1 = null,
|
|
||||||
kde_output_order_global_name: ?u32 = null,
|
|
||||||
|
|
||||||
/// Connector name of the primary output (e.g., "DP-1") as reported
|
|
||||||
/// by kde_output_order_v1. The first output in each priority list
|
|
||||||
/// is the primary.
|
|
||||||
primary_output_name: ?[:0]const u8 = null,
|
|
||||||
|
|
||||||
/// Tracks the output order event cycle. Set to true after a `done`
|
|
||||||
/// event so the next `output` event is captured as the new primary.
|
|
||||||
/// Initialized to true so the first event after binding is captured.
|
|
||||||
output_order_done: bool = true,
|
|
||||||
|
|
||||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
|
||||||
|
|
||||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
|
||||||
|
|
||||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
|
||||||
///
|
|
||||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
|
||||||
/// creates a quick terminal, and we need to ensure this fails
|
|
||||||
/// gracefully if this situation occurs.
|
|
||||||
///
|
|
||||||
/// FIXME: This is a temporary workaround - we should remove this when
|
|
||||||
/// all of our supported distros drop support for affected old
|
|
||||||
/// gtk4-layer-shell versions.
|
|
||||||
///
|
|
||||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
|
||||||
xdg_wm_dialog_present: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
@@ -86,39 +44,17 @@ pub const App = struct {
|
|||||||
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
|
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Create our context for our callbacks so we have a stable pointer.
|
const globals: *Globals = try .init(alloc, display);
|
||||||
// Note: at the time of writing this comment, we don't really need
|
errdefer globals.deinit();
|
||||||
// a stable pointer, but it's too scary that we'd need one in the future
|
|
||||||
// and not have it and corrupt memory or something so let's just do it.
|
|
||||||
const context = try alloc.create(Context);
|
|
||||||
errdefer {
|
|
||||||
if (context.primary_output_name) |name| alloc.free(name);
|
|
||||||
alloc.destroy(context);
|
|
||||||
}
|
|
||||||
context.* = .{ .alloc = alloc };
|
|
||||||
|
|
||||||
// Get our display registry so we can get all the available interfaces
|
|
||||||
// and bind to what we need.
|
|
||||||
const registry = try display.getRegistry();
|
|
||||||
registry.setListener(*Context, registryListener, context);
|
|
||||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
|
||||||
|
|
||||||
// Do another roundtrip to process events emitted by globals we bound
|
|
||||||
// during registry discovery (e.g. default decoration mode, output
|
|
||||||
// order). Listeners are installed at bind time in registryListener.
|
|
||||||
if (context.kde_decoration_manager != null or context.kde_output_order != null) {
|
|
||||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.display = display,
|
.display = display,
|
||||||
.context = context,
|
.globals = globals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
pub fn deinit(self: *App) void {
|
||||||
if (self.context.primary_output_name) |name| alloc.free(name);
|
self.globals.deinit();
|
||||||
alloc.destroy(self.context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eventMods(
|
pub fn eventMods(
|
||||||
@@ -130,22 +66,12 @@ pub const App = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn supportsQuickTerminal(self: App) bool {
|
pub fn supportsQuickTerminal(self: App) bool {
|
||||||
|
_ = self;
|
||||||
if (!layer_shell.isSupported()) {
|
if (!layer_shell.isSupported()) {
|
||||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.context.xdg_wm_dialog_present and
|
|
||||||
layer_shell.getLibraryVersion().order(.{
|
|
||||||
.major = 1,
|
|
||||||
.minor = 0,
|
|
||||||
.patch = 4,
|
|
||||||
}) == .lt)
|
|
||||||
{
|
|
||||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,239 +80,10 @@ pub const App = struct {
|
|||||||
layer_shell.initForWindow(window);
|
layer_shell.initForWindow(window);
|
||||||
|
|
||||||
// Set target monitor based on config (null lets compositor decide)
|
// Set target monitor based on config (null lets compositor decide)
|
||||||
const monitor = resolveQuickTerminalMonitor(self.context, apprt_window);
|
const monitor = resolveQuickTerminalMonitor(self.globals, apprt_window);
|
||||||
defer if (monitor) |v| v.unref();
|
defer if (monitor) |v| v.unref();
|
||||||
layer_shell.setMonitor(window, monitor);
|
layer_shell.setMonitor(window, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the quick-terminal-screen config to a specific monitor.
|
|
||||||
/// Returns null to let the compositor decide (used for .mouse mode).
|
|
||||||
/// Caller owns the returned ref and must unref it.
|
|
||||||
fn resolveQuickTerminalMonitor(
|
|
||||||
context: *Context,
|
|
||||||
apprt_window: *ApprtWindow,
|
|
||||||
) ?*gdk.Monitor {
|
|
||||||
const config = if (apprt_window.getConfig()) |v| v.get() else return null;
|
|
||||||
|
|
||||||
switch (config.@"quick-terminal-screen") {
|
|
||||||
.mouse => return null,
|
|
||||||
.main, .@"macos-menu-bar" => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const display = apprt_window.as(gtk.Widget).getDisplay();
|
|
||||||
const monitors = display.getMonitors();
|
|
||||||
|
|
||||||
// Try to find the monitor matching the primary output name.
|
|
||||||
if (context.primary_output_name) |stored_name| {
|
|
||||||
var i: u32 = 0;
|
|
||||||
while (monitors.getObject(i)) |item| : (i += 1) {
|
|
||||||
const monitor = gobject.ext.cast(gdk.Monitor, item) orelse {
|
|
||||||
item.unref();
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if (monitor.getConnector()) |connector_z| {
|
|
||||||
if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) {
|
|
||||||
return monitor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
monitor.unref();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to the first monitor in the list.
|
|
||||||
const first = monitors.getObject(0) orelse return null;
|
|
||||||
return gobject.ext.cast(gdk.Monitor, first) orelse {
|
|
||||||
first.unref();
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
|
||||||
// Globals should be optional pointers
|
|
||||||
const T = switch (@typeInfo(field.type)) {
|
|
||||||
.optional => |o| switch (@typeInfo(o.child)) {
|
|
||||||
.pointer => |v| if (v.size == .one) v.child else return null,
|
|
||||||
else => return null,
|
|
||||||
},
|
|
||||||
else => return null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only process Wayland interfaces
|
|
||||||
if (!@hasDecl(T, "interface")) return null;
|
|
||||||
return T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the Context field that stores the registry global name for
|
|
||||||
/// protocols that support replacement, or null for simple protocols.
|
|
||||||
fn getGlobalNameField(comptime field_name: []const u8) ?[]const u8 {
|
|
||||||
if (std.mem.eql(u8, field_name, "kde_decoration_manager")) {
|
|
||||||
return "kde_decoration_manager_global_name";
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, field_name, "kde_output_order")) {
|
|
||||||
return "kde_output_order_global_name";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset cached state derived from kde_output_order_v1.
|
|
||||||
fn resetOutputOrderState(context: *Context) void {
|
|
||||||
if (context.primary_output_name) |name| context.alloc.free(name);
|
|
||||||
context.primary_output_name = null;
|
|
||||||
context.output_order_done = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn registryListener(
|
|
||||||
registry: *wl.Registry,
|
|
||||||
event: wl.Registry.Event,
|
|
||||||
context: *Context,
|
|
||||||
) void {
|
|
||||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
.global => |v| {
|
|
||||||
log.debug("found global {s}", .{v.interface});
|
|
||||||
|
|
||||||
// We don't actually do anything with this other than checking
|
|
||||||
// for its existence, so we process this separately.
|
|
||||||
if (std.mem.orderZ(
|
|
||||||
u8,
|
|
||||||
v.interface,
|
|
||||||
"xdg_wm_dialog_v1",
|
|
||||||
) == .eq) {
|
|
||||||
context.xdg_wm_dialog_present = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline for (ctx_fields) |field| {
|
|
||||||
const T = getInterfaceType(field) orelse continue;
|
|
||||||
if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) {
|
|
||||||
log.debug("matched {}", .{T});
|
|
||||||
|
|
||||||
const global = registry.bind(
|
|
||||||
v.name,
|
|
||||||
T,
|
|
||||||
T.generated_version,
|
|
||||||
) catch |err| {
|
|
||||||
log.warn(
|
|
||||||
"error binding interface {s} error={}",
|
|
||||||
.{ v.interface, err },
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Destroy old binding if this global was re-advertised.
|
|
||||||
// Bind first so a failed bind preserves the old binding.
|
|
||||||
if (@field(context, field.name)) |old| {
|
|
||||||
old.destroy();
|
|
||||||
|
|
||||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
|
||||||
resetOutputOrderState(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@field(context, field.name) = global;
|
|
||||||
if (comptime getGlobalNameField(field.name)) |name_field| {
|
|
||||||
@field(context, name_field) = v.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install listeners immediately at bind time. This
|
|
||||||
// keeps listener setup and object lifetime in one
|
|
||||||
// place and also supports globals that appear later.
|
|
||||||
if (comptime std.mem.eql(u8, field.name, "kde_decoration_manager")) {
|
|
||||||
global.setListener(*Context, decoManagerListener, context);
|
|
||||||
}
|
|
||||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
|
||||||
global.setListener(*Context, outputOrderListener, context);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// This should be a rare occurrence, but in case a global
|
|
||||||
// is suddenly no longer available, we destroy and unset it
|
|
||||||
// as the protocol mandates.
|
|
||||||
.global_remove => |v| remove: {
|
|
||||||
inline for (ctx_fields) |field| {
|
|
||||||
if (getInterfaceType(field) == null) continue;
|
|
||||||
|
|
||||||
const global_name_field = comptime getGlobalNameField(field.name);
|
|
||||||
if (global_name_field) |name_field| {
|
|
||||||
if (@field(context, name_field)) |stored_name| {
|
|
||||||
if (stored_name == v.name) {
|
|
||||||
if (@field(context, field.name)) |global| global.destroy();
|
|
||||||
@field(context, field.name) = null;
|
|
||||||
@field(context, name_field) = null;
|
|
||||||
|
|
||||||
if (comptime std.mem.eql(u8, field.name, "kde_output_order")) {
|
|
||||||
resetOutputOrderState(context);
|
|
||||||
}
|
|
||||||
break :remove;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (@field(context, field.name)) |global| {
|
|
||||||
if (global.getId() == v.name) {
|
|
||||||
global.destroy();
|
|
||||||
@field(context, field.name) = null;
|
|
||||||
break :remove;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decoManagerListener(
|
|
||||||
_: *org.KdeKwinServerDecorationManager,
|
|
||||||
event: org.KdeKwinServerDecorationManager.Event,
|
|
||||||
context: *Context,
|
|
||||||
) void {
|
|
||||||
switch (event) {
|
|
||||||
.default_mode => |mode| {
|
|
||||||
context.default_deco_mode = @enumFromInt(mode.mode);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn outputOrderListener(
|
|
||||||
_: *kde.OutputOrderV1,
|
|
||||||
event: kde.OutputOrderV1.Event,
|
|
||||||
context: *Context,
|
|
||||||
) void {
|
|
||||||
switch (event) {
|
|
||||||
.output => |v| {
|
|
||||||
// Only the first output event after a `done` is the new primary.
|
|
||||||
if (!context.output_order_done) return;
|
|
||||||
context.output_order_done = false;
|
|
||||||
|
|
||||||
const name = std.mem.sliceTo(v.output_name, 0);
|
|
||||||
if (context.primary_output_name) |old| context.alloc.free(old);
|
|
||||||
|
|
||||||
if (name.len == 0) {
|
|
||||||
context.primary_output_name = null;
|
|
||||||
log.warn("ignoring empty primary output name from kde_output_order_v1", .{});
|
|
||||||
} else {
|
|
||||||
context.primary_output_name = context.alloc.dupeZ(u8, name) catch |err| {
|
|
||||||
context.primary_output_name = null;
|
|
||||||
log.warn("failed to allocate primary output name: {}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
log.debug("primary output: {s}", .{name});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.done => {
|
|
||||||
if (context.output_order_done) {
|
|
||||||
// No output arrived since the previous done. Treat this as
|
|
||||||
// an empty update and drop any stale cached primary.
|
|
||||||
resetOutputOrderState(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.output_order_done = true;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||||
@@ -397,7 +94,7 @@ pub const Window = struct {
|
|||||||
surface: *wl.Surface,
|
surface: *wl.Surface,
|
||||||
|
|
||||||
/// The context from the app where we can load our Wayland interfaces.
|
/// The context from the app where we can load our Wayland interfaces.
|
||||||
app_context: *App.Context,
|
globals: *Globals,
|
||||||
|
|
||||||
/// A token that, when present, indicates that the window is blurred.
|
/// A token that, when present, indicates that the window is blurred.
|
||||||
blur_token: ?*org.KdeKwinBlur = null,
|
blur_token: ?*org.KdeKwinBlur = null,
|
||||||
@@ -438,7 +135,7 @@ pub const Window = struct {
|
|||||||
// Get our decoration object so we can control the
|
// Get our decoration object so we can control the
|
||||||
// CSD vs SSD status of this surface.
|
// CSD vs SSD status of this surface.
|
||||||
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
||||||
const mgr = app.context.kde_decoration_manager orelse
|
const mgr = app.globals.get(.kde_decoration_manager) orelse
|
||||||
break :deco null;
|
break :deco null;
|
||||||
|
|
||||||
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
||||||
@@ -464,7 +161,7 @@ pub const Window = struct {
|
|||||||
return .{
|
return .{
|
||||||
.apprt_window = apprt_window,
|
.apprt_window = apprt_window,
|
||||||
.surface = wl_surface,
|
.surface = wl_surface,
|
||||||
.app_context = app.context,
|
.globals = app.globals,
|
||||||
.decoration = deco,
|
.decoration = deco,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -499,7 +196,7 @@ pub const Window = struct {
|
|||||||
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
|
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
|
||||||
// However, if we do not support SSDs (e.g. GNOME) then we should enable
|
// However, if we do not support SSDs (e.g. GNOME) then we should enable
|
||||||
// CSDs even if the user prefers SSDs.
|
// CSDs even if the user prefers SSDs.
|
||||||
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
|
.Server => if (self.globals.get(.kde_decoration_manager)) |_| false else true,
|
||||||
.None => false,
|
.None => false,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
@@ -511,7 +208,7 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||||
const activation = self.app_context.xdg_activation orelse return;
|
const activation = self.globals.get(.xdg_activation) orelse return;
|
||||||
|
|
||||||
// If there already is a token, destroy and unset it
|
// If there already is a token, destroy and unset it
|
||||||
if (self.activation_token) |token| token.destroy();
|
if (self.activation_token) |token| token.destroy();
|
||||||
@@ -527,7 +224,7 @@ pub const Window = struct {
|
|||||||
|
|
||||||
/// Update the blur state of the window.
|
/// Update the blur state of the window.
|
||||||
fn syncBlur(self: *Window) !void {
|
fn syncBlur(self: *Window) !void {
|
||||||
const manager = self.app_context.kde_blur_manager orelse return;
|
const manager = self.globals.get(.kde_blur_manager) orelse return;
|
||||||
const config = if (self.apprt_window.getConfig()) |v|
|
const config = if (self.apprt_window.getConfig()) |v|
|
||||||
v.get()
|
v.get()
|
||||||
else
|
else
|
||||||
@@ -561,7 +258,7 @@ pub const Window = struct {
|
|||||||
|
|
||||||
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
||||||
return switch (self.apprt_window.getWindowDecoration()) {
|
return switch (self.apprt_window.getWindowDecoration()) {
|
||||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
.auto => self.globals.state.default_deco_mode orelse .Client,
|
||||||
.client => .Client,
|
.client => .Client,
|
||||||
.server => .Server,
|
.server => .Server,
|
||||||
.none => .None,
|
.none => .None,
|
||||||
@@ -585,7 +282,7 @@ pub const Window = struct {
|
|||||||
|
|
||||||
// Re-resolve the target monitor on every sync so that config reloads
|
// Re-resolve the target monitor on every sync so that config reloads
|
||||||
// and primary-output changes take effect without recreating the window.
|
// and primary-output changes take effect without recreating the window.
|
||||||
const target_monitor = App.resolveQuickTerminalMonitor(self.app_context, self.apprt_window);
|
const target_monitor = resolveQuickTerminalMonitor(self.globals, self.apprt_window);
|
||||||
defer if (target_monitor) |v| v.unref();
|
defer if (target_monitor) |v| v.unref();
|
||||||
layer_shell.setMonitor(window, target_monitor);
|
layer_shell.setMonitor(window, target_monitor);
|
||||||
|
|
||||||
@@ -629,7 +326,7 @@ pub const Window = struct {
|
|||||||
if (self.slide) |slide| slide.release();
|
if (self.slide) |slide| slide.release();
|
||||||
|
|
||||||
self.slide = if (anchored_edge) |anchored| slide: {
|
self.slide = if (anchored_edge) |anchored| slide: {
|
||||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
const mgr = self.globals.get(.kde_slide_manager) orelse break :slide null;
|
||||||
|
|
||||||
const slide = mgr.create(self.surface) catch |err| {
|
const slide = mgr.create(self.surface) catch |err| {
|
||||||
log.warn("could not create slide object={}", .{err});
|
log.warn("could not create slide object={}", .{err});
|
||||||
@@ -658,8 +355,8 @@ pub const Window = struct {
|
|||||||
const window = apprt_window.as(gtk.Window);
|
const window = apprt_window.as(gtk.Window);
|
||||||
const config = if (apprt_window.getConfig()) |v| v.get() else return;
|
const config = if (apprt_window.getConfig()) |v| v.get() else return;
|
||||||
|
|
||||||
const resolved_monitor = App.resolveQuickTerminalMonitor(
|
const resolved_monitor = resolveQuickTerminalMonitor(
|
||||||
apprt_window.winproto().wayland.app_context,
|
apprt_window.winproto().wayland.globals,
|
||||||
apprt_window,
|
apprt_window,
|
||||||
);
|
);
|
||||||
defer if (resolved_monitor) |v| v.unref();
|
defer if (resolved_monitor) |v| v.unref();
|
||||||
@@ -686,7 +383,7 @@ pub const Window = struct {
|
|||||||
event: xdg.ActivationTokenV1.Event,
|
event: xdg.ActivationTokenV1.Event,
|
||||||
self: *Window,
|
self: *Window,
|
||||||
) void {
|
) void {
|
||||||
const activation = self.app_context.xdg_activation orelse return;
|
const activation = self.globals.get(.xdg_activation) orelse return;
|
||||||
const current_token = self.activation_token orelse return;
|
const current_token = self.activation_token orelse return;
|
||||||
|
|
||||||
if (token.getId() != current_token.getId()) {
|
if (token.getId() != current_token.getId()) {
|
||||||
@@ -703,3 +400,45 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Resolve the quick-terminal-screen config to a specific monitor.
|
||||||
|
/// Returns null to let the compositor decide (used for .mouse mode).
|
||||||
|
/// Caller owns the returned ref and must unref it.
|
||||||
|
fn resolveQuickTerminalMonitor(
|
||||||
|
globals: *Globals,
|
||||||
|
apprt_window: *ApprtWindow,
|
||||||
|
) ?*gdk.Monitor {
|
||||||
|
const config = if (apprt_window.getConfig()) |v| v.get() else return null;
|
||||||
|
|
||||||
|
switch (config.@"quick-terminal-screen") {
|
||||||
|
.mouse => return null,
|
||||||
|
.main, .@"macos-menu-bar" => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const display = apprt_window.as(gtk.Widget).getDisplay();
|
||||||
|
const monitors = display.getMonitors();
|
||||||
|
|
||||||
|
// Try to find the monitor matching the primary output name.
|
||||||
|
if (globals.state.primary_output_name) |stored_name| {
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (monitors.getObject(i)) |item| : (i += 1) {
|
||||||
|
const monitor = gobject.ext.cast(gdk.Monitor, item) orelse {
|
||||||
|
item.unref();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (monitor.getConnector()) |connector_z| {
|
||||||
|
if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) {
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monitor.unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the first monitor in the list.
|
||||||
|
const first = monitors.getObject(0) orelse return null;
|
||||||
|
return gobject.ext.cast(gdk.Monitor, first) orelse {
|
||||||
|
first.unref();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
230
src/apprt/gtk/winproto/wayland/Globals.zig
Normal file
230
src/apprt/gtk/winproto/wayland/Globals.zig
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
const Globals = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const wayland = @import("wayland");
|
||||||
|
const wl = wayland.client.wl;
|
||||||
|
const kde = wayland.client.kde;
|
||||||
|
const org = wayland.client.org;
|
||||||
|
const xdg = wayland.client.xdg;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.winproto_wayland_globals);
|
||||||
|
|
||||||
|
alloc: Allocator,
|
||||||
|
state: State,
|
||||||
|
map: std.EnumMap(Tag, Binding),
|
||||||
|
|
||||||
|
/// Used in the initial roundtrip to determine whether more
|
||||||
|
/// roundtrips are required to fetch the initial state.
|
||||||
|
needs_roundtrip: bool = false,
|
||||||
|
|
||||||
|
const Binding = struct {
|
||||||
|
// All globals can be casted into a wl.Proxy object.
|
||||||
|
proxy: *wl.Proxy,
|
||||||
|
name: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tag = enum {
|
||||||
|
kde_blur_manager,
|
||||||
|
kde_decoration_manager,
|
||||||
|
kde_slide_manager,
|
||||||
|
kde_output_order,
|
||||||
|
xdg_activation,
|
||||||
|
|
||||||
|
fn Type(comptime self: Tag) type {
|
||||||
|
return switch (self) {
|
||||||
|
.kde_blur_manager => org.KdeKwinBlurManager,
|
||||||
|
.kde_decoration_manager => org.KdeKwinServerDecorationManager,
|
||||||
|
.kde_slide_manager => org.KdeKwinSlideManager,
|
||||||
|
.kde_output_order => kde.OutputOrderV1,
|
||||||
|
.xdg_activation => xdg.ActivationV1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const State = struct {
|
||||||
|
/// Connector name of the primary output (e.g., "DP-1") as reported
|
||||||
|
/// by kde_output_order_v1. The first output in each priority list
|
||||||
|
/// is the primary.
|
||||||
|
primary_output_name: ?[:0]const u8 = null,
|
||||||
|
|
||||||
|
/// Tracks the output order event cycle. Set to true after a `done`
|
||||||
|
/// event so the next `output` event is captured as the new primary.
|
||||||
|
/// Initialized to true so the first event after binding is captured.
|
||||||
|
output_order_done: bool = true,
|
||||||
|
|
||||||
|
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||||
|
|
||||||
|
/// Reset cached state derived from kde_output_order_v1.
|
||||||
|
fn resetOutputOrder(self: *State, alloc: Allocator) void {
|
||||||
|
if (self.primary_output_name) |name| alloc.free(name);
|
||||||
|
self.primary_output_name = null;
|
||||||
|
self.output_order_done = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(alloc: Allocator, display: *wl.Display) !*Globals {
|
||||||
|
// We need to allocate here since the listener
|
||||||
|
// expects a stable memory address.
|
||||||
|
const self = try alloc.create(Globals);
|
||||||
|
self.* = .{
|
||||||
|
.alloc = alloc,
|
||||||
|
.state = .{},
|
||||||
|
.map = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
const registry = try display.getRegistry();
|
||||||
|
registry.setListener(*Globals, registryListener, self);
|
||||||
|
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||||
|
|
||||||
|
// Do another roundtrip to process events emitted by globals we bound
|
||||||
|
// during registry discovery (e.g. default decoration mode, output
|
||||||
|
// order). Listeners are installed at bind time in registryListener.
|
||||||
|
if (self.needs_roundtrip) {
|
||||||
|
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Globals) void {
|
||||||
|
if (self.state.primary_output_name) |name| self.alloc.free(name);
|
||||||
|
self.alloc.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *const Globals, comptime tag: Tag) ?*tag.Type() {
|
||||||
|
const binding = self.map.get(tag) orelse return null;
|
||||||
|
return @ptrCast(binding.proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onGlobalAttached(self: *Globals, comptime tag: Tag) void {
|
||||||
|
// Install listeners immediately at bind time. This
|
||||||
|
// keeps listener setup and object lifetime in one
|
||||||
|
// place and also supports globals that appear later.
|
||||||
|
switch (tag) {
|
||||||
|
.kde_decoration_manager => {
|
||||||
|
const v = self.get(tag) orelse return;
|
||||||
|
v.setListener(*Globals, decoManagerListener, self);
|
||||||
|
self.needs_roundtrip = true;
|
||||||
|
},
|
||||||
|
.kde_output_order => {
|
||||||
|
const v = self.get(tag) orelse return;
|
||||||
|
v.setListener(*Globals, outputOrderListener, self);
|
||||||
|
self.needs_roundtrip = true;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onGlobalRemoved(self: *Globals, tag: Tag) void {
|
||||||
|
switch (tag) {
|
||||||
|
.kde_output_order => self.state.resetOutputOrder(self.alloc),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registryListener(
|
||||||
|
registry: *wl.Registry,
|
||||||
|
event: wl.Registry.Event,
|
||||||
|
self: *Globals,
|
||||||
|
) void {
|
||||||
|
switch (event) {
|
||||||
|
.global => |v| {
|
||||||
|
log.debug("found global {s}", .{v.interface});
|
||||||
|
inline for (comptime std.meta.tags(Tag)) |tag| {
|
||||||
|
const T = tag.Type();
|
||||||
|
if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) {
|
||||||
|
log.debug("matched {}", .{T});
|
||||||
|
|
||||||
|
const new_proxy = registry.bind(
|
||||||
|
v.name,
|
||||||
|
T,
|
||||||
|
T.generated_version,
|
||||||
|
) catch |err| {
|
||||||
|
log.warn(
|
||||||
|
"error binding interface {s} error={}",
|
||||||
|
.{ v.interface, err },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this global was already bound,
|
||||||
|
// then we also need to destroy the old binding.
|
||||||
|
if (self.map.get(tag)) |old| {
|
||||||
|
self.onGlobalRemoved(tag);
|
||||||
|
old.proxy.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.map.put(tag, .{
|
||||||
|
.proxy = @ptrCast(new_proxy),
|
||||||
|
.name = v.name,
|
||||||
|
});
|
||||||
|
self.onGlobalAttached(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// This should be a rare occurrence, but in case a global
|
||||||
|
// is suddenly no longer available, we destroy and unset it
|
||||||
|
// as the protocol mandates.
|
||||||
|
.global_remove => |v| {
|
||||||
|
var it = self.map.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
if (kv.value.name != v.name) continue;
|
||||||
|
self.onGlobalRemoved(kv.key);
|
||||||
|
kv.value.proxy.destroy();
|
||||||
|
self.map.remove(kv.key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decoManagerListener(
|
||||||
|
_: *org.KdeKwinServerDecorationManager,
|
||||||
|
event: org.KdeKwinServerDecorationManager.Event,
|
||||||
|
self: *Globals,
|
||||||
|
) void {
|
||||||
|
switch (event) {
|
||||||
|
.default_mode => |mode| {
|
||||||
|
self.state.default_deco_mode = @enumFromInt(mode.mode);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outputOrderListener(
|
||||||
|
_: *kde.OutputOrderV1,
|
||||||
|
event: kde.OutputOrderV1.Event,
|
||||||
|
self: *Globals,
|
||||||
|
) void {
|
||||||
|
switch (event) {
|
||||||
|
.output => |v| {
|
||||||
|
// Only the first output event after a `done` is the new primary.
|
||||||
|
if (!self.state.output_order_done) return;
|
||||||
|
self.state.output_order_done = false;
|
||||||
|
|
||||||
|
const name = std.mem.sliceTo(v.output_name, 0);
|
||||||
|
if (self.state.primary_output_name) |old| self.alloc.free(old);
|
||||||
|
|
||||||
|
if (name.len == 0) {
|
||||||
|
self.state.primary_output_name = null;
|
||||||
|
log.warn("ignoring empty primary output name from kde_output_order_v1", .{});
|
||||||
|
} else {
|
||||||
|
self.state.primary_output_name = self.alloc.dupeZ(u8, name) catch |err| {
|
||||||
|
self.state.primary_output_name = null;
|
||||||
|
log.warn("failed to allocate primary output name: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log.debug("primary output: {s}", .{name});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.done => {
|
||||||
|
if (self.state.output_order_done) {
|
||||||
|
// No output arrived since the previous done. Treat this as
|
||||||
|
// an empty update and drop any stale cached primary.
|
||||||
|
self.state.resetOutputOrder(self.alloc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.state.output_order_done = true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,9 +106,8 @@ pub const App = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
pub fn deinit(self: *App) void {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = alloc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks for an immediate pending XKB state update event, and returns the
|
/// Checks for an immediate pending XKB state update event, and returns the
|
||||||
|
|||||||
Reference in New Issue
Block a user