Leah Amelia Chen 4cd49632b2 gtk(x11): support server-side decorations
Remind me to never touch Xlib code ever again.
2025-02-03 18:56:09 +01:00

482 lines
15 KiB
Zig

//! X11 window protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
const Allocator = std.mem.Allocator;
const c = @import("../c.zig").c;
const input = @import("../../../input.zig");
const Config = @import("../../../config.zig").Config;
const adwaita = @import("../adwaita.zig");
const log = std.log.scoped(.gtk_x11);
pub const App = struct {
display: *c.Display,
base_event_code: c_int,
atoms: Atoms,
pub fn init(
alloc: Allocator,
gdk_display: *c.GdkDisplay,
app_id: [:0]const u8,
config: *const Config,
) !?App {
_ = alloc;
// If the display isn't X11, then we don't need to do anything.
if (c.g_type_check_instance_is_a(
@ptrCast(@alignCast(gdk_display)),
c.gdk_x11_display_get_type(),
) == 0) return null;
// Get our X11 display
const display: *c.Display = c.gdk_x11_display_get_xdisplay(
gdk_display,
) orelse return error.NoX11Display;
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
pn
else if (builtin.mode == .Debug)
"ghostty-debug"
else
"ghostty";
// Set the X11 window class property (WM_CLASS) if are are on an X11
// display.
//
// Note that we also set the program name here using g_set_prgname.
// This is how the instance name field for WM_CLASS is derived when
// calling gdk_x11_display_set_program_class; there does not seem to be
// a way to set it directly. It does not look like this is being set by
// our other app initialization routines currently, but since we're
// currently deriving its value from x11-instance-name effectively, I
// feel like gating it behind an X11 check is better intent.
//
// This makes the property show up like so when using xprop:
//
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
//
// Append "-debug" on both when using the debug build.
c.g_set_prgname(x11_program_name);
c.gdk_x11_display_set_program_class(gdk_display, app_id);
// XKB
log.debug("Xkb.init: initializing Xkb", .{});
log.debug("Xkb.init: running XkbQueryExtension", .{});
var opcode: c_int = 0;
var base_event_code: c_int = 0;
var base_error_code: c_int = 0;
var major = c.XkbMajorVersion;
var minor = c.XkbMinorVersion;
if (c.XkbQueryExtension(
display,
&opcode,
&base_event_code,
&base_error_code,
&major,
&minor,
) == 0) {
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
return error.XkbInitializationError;
}
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
if (c.XkbSelectEventDetails(
display,
c.XkbUseCoreKbd,
c.XkbStateNotify,
c.XkbModifierStateMask,
c.XkbModifierStateMask,
) == 0) {
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
return error.XkbInitializationError;
}
return .{
.display = display,
.base_event_code = base_event_code,
.atoms = Atoms.init(gdk_display),
};
}
pub fn deinit(self: *App, alloc: Allocator) void {
_ = self;
_ = alloc;
}
/// Checks for an immediate pending XKB state update event, and returns the
/// keyboard state based on if it finds any. This is necessary as the
/// standard GTK X11 API (and X11 in general) does not include the current
/// key pressed in any modifier state snapshot for that event (e.g. if the
/// pressed key is a modifier, that is not necessarily reflected in the
/// modifiers).
///
/// Returns null if there is no event. In this case, the caller should fall
/// back to the standard GDK modifier state (this likely means the key
/// event did not result in a modifier change).
pub fn eventMods(
self: App,
device: ?*c.GdkDevice,
gtk_mods: c.GdkModifierType,
) ?input.Mods {
_ = device;
_ = gtk_mods;
// Shoutout to Mozilla for figuring out a clean way to do this, this is
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
if (c.XEventsQueued(self.display, c.QueuedAfterReading) == 0) return null;
var nextEvent: c.XEvent = undefined;
_ = c.XPeekEvent(self.display, &nextEvent);
if (nextEvent.type != self.base_event_code) return null;
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
// Check the state according to XKB masks.
const lookup_mods = xkb_state_notify_event.lookup_mods;
var mods: input.Mods = .{};
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
return mods;
}
};
pub const Window = struct {
app: *App,
alloc: Allocator,
config: DerivedConfig,
window: c.Window,
gtk_window: *c.GtkWindow,
blur_region: Region = .{},
const DerivedConfig = struct {
blur: bool,
window_decoration: Config.WindowDecoration,
pub fn init(config: *const Config) DerivedConfig {
return .{
.blur = config.@"background-blur".enabled(),
.window_decoration = config.@"window-decoration",
};
}
};
pub fn init(
alloc: Allocator,
app: *App,
gtk_window: *c.GtkWindow,
config: *const Config,
) !Window {
const surface = c.gtk_native_get_surface(
@ptrCast(gtk_window),
) orelse return error.NotX11Surface;
// Check if we're actually on X11
if (c.g_type_check_instance_is_a(
@ptrCast(@alignCast(surface)),
c.gdk_x11_surface_get_type(),
) == 0) return error.NotX11Surface;
return .{
.app = app,
.alloc = alloc,
.config = DerivedConfig.init(config),
.window = c.gdk_x11_surface_get_xid(surface),
.gtk_window = gtk_window,
};
}
pub fn deinit(self: Window, alloc: Allocator) void {
_ = self;
_ = alloc;
}
pub fn updateConfigEvent(
self: *Window,
config: *const Config,
) !void {
self.config = DerivedConfig.init(config);
}
pub fn resizeEvent(self: *Window) !void {
// The blur region must update with window resizes
self.blur_region.width = c.gtk_widget_get_width(@ptrCast(self.gtk_window));
self.blur_region.height = c.gtk_widget_get_height(@ptrCast(self.gtk_window));
try self.syncBlur();
}
pub fn syncAppearance(self: *Window) !void {
self.blur_region = blur: {
// NOTE(pluiedev): CSDs are a f--king mistake.
// Please, GNOME, stop this nonsense of making a window ~30% bigger
// internally than how they really are just for your shadows and
// rounded corners and all that fluff. Please. I beg of you.
var x: f64 = 0;
var y: f64 = 0;
c.gtk_native_get_surface_transform(
@ptrCast(self.gtk_window),
&x,
&y,
);
break :blur .{
.x = @intFromFloat(x),
.y = @intFromFloat(y),
};
};
self.syncBlur() catch |err| {
log.err("failed to synchronize blur={}", .{err});
};
self.syncDecorations() catch |err| {
log.err("failed to synchronize decorations={}", .{err});
};
}
pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self.config.window_decoration) {
.auto, .client => true,
.server, .none => false,
};
}
fn syncBlur(self: *Window) !void {
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
// which means that the blur region will grow slightly outside of the
// window borders. Unfortunately, actually calculating the rounded
// region can be quite complex without having access to existing APIs
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
// and I think it's not really noticeable enough to justify the effort.
// (Wayland also has this visual artifact anyway...)
const blur = self.config.blur;
log.debug("set blur={}, window xid={}, region={}", .{
blur,
self.window,
self.blur_region,
});
if (blur) {
try self.changeProperty(
Region,
self.app.atoms.kde_blur,
c.XA_CARDINAL,
._32,
.{ .mode = .replace },
&self.blur_region,
);
} else {
try self.deleteProperty(self.app.atoms.kde_blur);
}
}
fn syncDecorations(self: *Window) !void {
var hints: MotifWMHints = .{};
self.getWindowProperty(
MotifWMHints,
self.app.atoms.motif_wm_hints,
self.app.atoms.motif_wm_hints,
._32,
.{},
&hints,
) catch |err| switch (err) {
// motif_wm_hints is already initialized, so this is fine
error.PropertyNotFound => {},
error.RequestFailed,
error.PropertyTypeMismatch,
error.PropertyFormatMismatch,
=> return err,
};
hints.flags.decorations = true;
hints.decorations.all = switch (self.config.window_decoration) {
.server => true,
.auto, .client, .none => false,
};
try self.changeProperty(
MotifWMHints,
self.app.atoms.motif_wm_hints,
self.app.atoms.motif_wm_hints,
._32,
.{ .mode = .replace },
&hints,
);
}
fn getWindowProperty(
self: *Window,
comptime T: type,
name: c.Atom,
typ: c.Atom,
comptime format: PropertyFormat,
options: struct {
offset: c_long = 0,
length: c_long = std.math.maxInt(c_long),
delete: bool = false,
},
result: *T,
) GetWindowPropertyError!void {
// FIXME: Maybe we should switch to libxcb one day.
// Sounds like a much better idea than whatever this is
var actual_type_return: c.Atom = undefined;
var actual_format_return: c_int = undefined;
var nitems_return: c_ulong = undefined;
var bytes_after_return: c_ulong = undefined;
var prop_return: ?format.bufferType() = null;
const code = c.XGetWindowProperty(
self.app.display,
self.window,
name,
options.offset,
options.length,
@intFromBool(options.delete),
typ,
&actual_type_return,
&actual_format_return,
&nitems_return,
&bytes_after_return,
&prop_return,
);
if (code != c.Success) return error.RequestFailed;
if (actual_type_return == c.None) return error.PropertyNotFound;
if (typ != actual_type_return) return error.PropertyTypeMismatch;
if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
const data_ptr: *T = @ptrCast(prop_return);
result.* = data_ptr.*;
_ = c.XFree(prop_return);
}
fn changeProperty(
self: *Window,
comptime T: type,
name: c.Atom,
typ: c.Atom,
comptime format: PropertyFormat,
options: struct {
mode: PropertyChangeMode,
},
value: *T,
) X11Error!void {
const data: format.bufferType() = @ptrCast(value);
const status = c.XChangeProperty(
self.app.display,
self.window,
name,
typ,
@intFromEnum(format),
@intFromEnum(options.mode),
data,
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
);
// For some godforsaken reason Xlib alternates between
// error values (0 = success) and booleans (1 = success), and they look exactly
// the same in the signature (just `int`, since Xlib is written in C89)...
if (status == 0) return error.RequestFailed;
}
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
const status = c.XDeleteProperty(self.app.display, self.window, name);
if (status == 0) return error.RequestFailed;
}
};
const X11Error = error{
RequestFailed,
};
const GetWindowPropertyError = X11Error || error{
PropertyNotFound,
PropertyTypeMismatch,
PropertyFormatMismatch,
};
const Atoms = struct {
kde_blur: c.Atom,
motif_wm_hints: c.Atom,
fn init(display: *c.GdkDisplay) Atoms {
return .{
.kde_blur = c.gdk_x11_get_xatom_by_name_for_display(
display,
"_KDE_NET_WM_BLUR_BEHIND_REGION",
),
.motif_wm_hints = c.gdk_x11_get_xatom_by_name_for_display(
display,
"_MOTIF_WM_HINTS",
),
};
}
};
const PropertyChangeMode = enum(c_int) {
replace = c.PropModeReplace,
prepend = c.PropModePrepend,
append = c.PropModeAppend,
};
const PropertyFormat = enum(c_int) {
_8 = 8,
_16 = 16,
_32 = 32,
fn elemType(comptime self: PropertyFormat) type {
return switch (self) {
._8 => c_char,
._16 => c_int,
._32 => c_long,
};
}
fn bufferType(comptime self: PropertyFormat) type {
// The buffer type has to be a multi-pointer to bytes
// *aligned to the element type* (very important,
// otherwise you'll read garbage!)
//
// I know this is really ugly. X11 is ugly. I consider it apropos.
return [*]align(@alignOf(self.elemType())) u8;
}
};
const Region = extern struct {
x: c_long = 0,
y: c_long = 0,
width: c_long = 0,
height: c_long = 0,
};
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
const MotifWMHints = extern struct {
flags: packed struct(c_ulong) {
_pad: u1 = 0,
decorations: bool = false,
// We don't really care about the other flags
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
} = .{},
functions: c_ulong = 0,
decorations: packed struct(c_ulong) {
all: bool = false,
// We don't really care about the other flags
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
} = .{},
input_mode: c_long = 0,
status: c_ulong = 0,
};