mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 05:06:24 +03:00
482 lines
15 KiB
Zig
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,
|
|
};
|