gtk(x11): support server-side decorations

Remind me to never touch Xlib code ever again.
This commit is contained in:
Leah Amelia Chen
2025-02-02 22:24:48 +01:00
parent c5508e7d19
commit 4cd49632b2

View File

@ -13,7 +13,7 @@ const log = std.log.scoped(.gtk_x11);
pub const App = struct {
display: *c.Display,
base_event_code: c_int,
kde_blur_atom: c.Atom,
atoms: Atoms,
pub fn init(
alloc: Allocator,
@ -95,10 +95,7 @@ pub const App = struct {
return .{
.display = display,
.base_event_code = base_event_code,
.kde_blur_atom = c.gdk_x11_get_xatom_by_name_for_display(
gdk_display,
"_KDE_NET_WM_BLUR_BEHIND_REGION",
),
.atoms = Atoms.init(gdk_display),
};
}
@ -154,28 +151,27 @@ pub const App = struct {
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,
has_decoration: bool,
window_decoration: Config.WindowDecoration,
pub fn init(config: *const Config) DerivedConfig {
return .{
.blur = config.@"background-blur".enabled(),
.has_decoration = switch (config.@"window-decoration") {
.none => false,
.auto, .client, .server => true,
},
.window_decoration = config.@"window-decoration",
};
}
};
pub fn init(
_: Allocator,
alloc: Allocator,
app: *App,
gtk_window: *c.GtkWindow,
config: *const Config,
@ -192,6 +188,7 @@ pub const Window = struct {
return .{
.app = app,
.alloc = alloc,
.config = DerivedConfig.init(config),
.window = c.gdk_x11_surface_get_xid(surface),
.gtk_window = gtk_window,
@ -236,11 +233,19 @@ pub const Window = struct {
.y = @intFromFloat(y),
};
};
try self.syncBlur();
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 self.config.has_decoration;
return switch (self.config.window_decoration) {
.auto, .client => true,
.server, .none => false,
};
}
fn syncBlur(self: *Window) !void {
@ -260,33 +265,192 @@ pub const Window = struct {
});
if (blur) {
_ = c.XChangeProperty(
self.app.display,
self.window,
self.app.kde_blur_atom,
try self.changeProperty(
Region,
self.app.atoms.kde_blur,
c.XA_CARDINAL,
// Despite what you might think, the "32" here does NOT mean
// that the data should be in u32s. Instead, they should be
// c_longs, which on any 64-bit architecture would be obviously
// 64 bits. WTF?!
32,
c.PropModeReplace,
// SAFETY: Region is an extern struct that has the same
// representation of 4 c_longs put next to each other.
// Therefore, reinterpretation should be safe.
// We don't have to care about endianness either since
// Xlib converts it to network byte order for us.
@ptrCast(std.mem.asBytes(&self.blur_region)),
4,
._32,
.{ .mode = .replace },
&self.blur_region,
);
} else {
_ = c.XDeleteProperty(
self.app.display,
self.window,
self.app.kde_blur_atom,
);
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 {
@ -295,3 +459,23 @@ const Region = extern struct {
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,
};