mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
gtk: unify Wayland and X11 platforms, implement background blur for KDE X11 (#4723)
Part of #4626. Please let me know if the architecture looks alright — I'm fairly convinced that I'm being unorthodox here.
This commit is contained in:
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@ -342,7 +342,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
adwaita: ["true", "false"]
|
adwaita: ["true", "false"]
|
||||||
x11: ["true", "false"]
|
x11: ["true", "false"]
|
||||||
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }}
|
wayland: ["true", "false"]
|
||||||
|
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
|
||||||
runs-on: namespace-profile-ghostty-sm
|
runs-on: namespace-profile-ghostty-sm
|
||||||
needs: test
|
needs: test
|
||||||
env:
|
env:
|
||||||
@ -374,7 +375,8 @@ jobs:
|
|||||||
zig build \
|
zig build \
|
||||||
-Dapp-runtime=gtk \
|
-Dapp-runtime=gtk \
|
||||||
-Dgtk-adwaita=${{ matrix.adwaita }} \
|
-Dgtk-adwaita=${{ matrix.adwaita }} \
|
||||||
-Dgtk-x11=${{ matrix.x11 }}
|
-Dgtk-x11=${{ matrix.x11 }} \
|
||||||
|
-Dgtk-wayland=${{ matrix.wayland }}
|
||||||
|
|
||||||
test-sentry-linux:
|
test-sentry-linux:
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -36,8 +36,7 @@ const c = @import("c.zig").c;
|
|||||||
const version = @import("version.zig");
|
const version = @import("version.zig");
|
||||||
const inspector = @import("inspector.zig");
|
const inspector = @import("inspector.zig");
|
||||||
const key = @import("key.zig");
|
const key = @import("key.zig");
|
||||||
const x11 = @import("x11.zig");
|
const winproto = @import("winproto.zig");
|
||||||
const wayland = @import("wayland.zig");
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
const log = std.log.scoped(.gtk);
|
||||||
@ -50,6 +49,9 @@ config: Config,
|
|||||||
app: *c.GtkApplication,
|
app: *c.GtkApplication,
|
||||||
ctx: *c.GMainContext,
|
ctx: *c.GMainContext,
|
||||||
|
|
||||||
|
/// State and logic for the underlying windowing protocol.
|
||||||
|
winproto: winproto.App,
|
||||||
|
|
||||||
/// True if the app was launched with single instance mode.
|
/// True if the app was launched with single instance mode.
|
||||||
single_instance: bool,
|
single_instance: bool,
|
||||||
|
|
||||||
@ -71,12 +73,6 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
|||||||
/// This is set to false when the main loop should exit.
|
/// This is set to false when the main loop should exit.
|
||||||
running: bool = true,
|
running: bool = true,
|
||||||
|
|
||||||
/// Xkb state (X11 only). Will be null on Wayland.
|
|
||||||
x11_xkb: ?x11.Xkb = null,
|
|
||||||
|
|
||||||
/// Wayland app state. Will be null on X11.
|
|
||||||
wayland: ?wayland.AppState = null,
|
|
||||||
|
|
||||||
/// The base path of the transient cgroup used to put all surfaces
|
/// The base path of the transient cgroup used to put all surfaces
|
||||||
/// into their own cgroup. This is only set if cgroups are enabled
|
/// into their own cgroup. This is only set if cgroups are enabled
|
||||||
/// and initialization was successful.
|
/// and initialization was successful.
|
||||||
@ -166,7 +162,12 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.gtk_init();
|
c.gtk_init();
|
||||||
const display = c.gdk_display_get_default();
|
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
|
||||||
|
// I'm unsure of any scenario where this happens. Because we don't
|
||||||
|
// want to litter null checks everywhere, we just exit here.
|
||||||
|
log.warn("gdk display is null, exiting", .{});
|
||||||
|
std.posix.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
// If we're using libadwaita, log the version
|
// If we're using libadwaita, log the version
|
||||||
if (adwaita.enabled(&config)) {
|
if (adwaita.enabled(&config)) {
|
||||||
@ -364,46 +365,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
return error.GtkApplicationRegisterFailed;
|
return error.GtkApplicationRegisterFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform all X11 initialization. This ultimately returns the X11
|
// Setup our windowing protocol logic
|
||||||
// keyboard state but the block does more than that (i.e. setting up
|
var winproto_app = try winproto.App.init(
|
||||||
// WM_CLASS).
|
core_app.alloc,
|
||||||
const x11_xkb: ?x11.Xkb = x11_xkb: {
|
display,
|
||||||
if (comptime !build_options.x11) break :x11_xkb null;
|
app_id,
|
||||||
if (!x11.is_display(display)) break :x11_xkb null;
|
&config,
|
||||||
|
);
|
||||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
errdefer winproto_app.deinit(core_app.alloc);
|
||||||
// display.
|
log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
const prgname = if (config.@"x11-instance-name") |pn|
|
|
||||||
pn
|
|
||||||
else if (builtin.mode == .Debug)
|
|
||||||
"ghostty-debug"
|
|
||||||
else
|
|
||||||
"ghostty";
|
|
||||||
c.g_set_prgname(prgname);
|
|
||||||
c.gdk_x11_display_set_program_class(display, app_id);
|
|
||||||
|
|
||||||
// Set up Xkb
|
|
||||||
break :x11_xkb try x11.Xkb.init(display);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize Wayland state
|
|
||||||
var wl = wayland.AppState.init(display);
|
|
||||||
if (wl) |*w| try w.register();
|
|
||||||
|
|
||||||
// This just calls the `activate` signal but its part of the normal startup
|
// This just calls the `activate` signal but its part of the normal startup
|
||||||
// routine so we just call it, but only if the config allows it (this allows
|
// routine so we just call it, but only if the config allows it (this allows
|
||||||
@ -429,8 +399,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
.config = config,
|
.config = config,
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.cursor_none = cursor_none,
|
.cursor_none = cursor_none,
|
||||||
.x11_xkb = x11_xkb,
|
.winproto = winproto_app,
|
||||||
.wayland = wl,
|
|
||||||
.single_instance = single_instance,
|
.single_instance = single_instance,
|
||||||
// If we are NOT the primary instance, then we never want to run.
|
// If we are NOT the primary instance, then we never want to run.
|
||||||
// This means that another instance of the GTK app is running and
|
// This means that another instance of the GTK app is running and
|
||||||
@ -458,6 +427,8 @@ pub fn terminate(self: *App) void {
|
|||||||
}
|
}
|
||||||
self.custom_css_providers.deinit(self.core_app.alloc);
|
self.custom_css_providers.deinit(self.core_app.alloc);
|
||||||
|
|
||||||
|
self.winproto.deinit(self.core_app.alloc);
|
||||||
|
|
||||||
self.config.deinit();
|
self.config.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -882,9 +853,10 @@ fn configChange(
|
|||||||
new_config: *const Config,
|
new_config: *const Config,
|
||||||
) void {
|
) void {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
.surface => |surface| {
|
.surface => |surface| surface: {
|
||||||
if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| {
|
const window = surface.rt_surface.container.window() orelse break :surface;
|
||||||
log.warn("error syncing appearance changes to window err={}", .{err});
|
window.updateConfig(new_config) catch |err| {
|
||||||
|
log.warn("error updating config for window err={}", .{err});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ const ResizeOverlay = @import("ResizeOverlay.zig");
|
|||||||
const inspector = @import("inspector.zig");
|
const inspector = @import("inspector.zig");
|
||||||
const gtk_key = @import("key.zig");
|
const gtk_key = @import("key.zig");
|
||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
const x11 = @import("x11.zig");
|
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_surface);
|
const log = std.log.scoped(.gtk_surface);
|
||||||
|
|
||||||
@ -1384,6 +1383,14 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (self.container.window()) |window| {
|
||||||
|
if (window.winproto) |*winproto| {
|
||||||
|
winproto.resizeEvent() catch |err| {
|
||||||
|
log.warn("failed to notify window protocol of resize={}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.resize_overlay.maybeShow();
|
self.resize_overlay.maybeShow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1699,11 +1706,10 @@ pub fn keyEvent(
|
|||||||
|
|
||||||
// Get our modifier for the event
|
// Get our modifier for the event
|
||||||
const mods: input.Mods = gtk_key.eventMods(
|
const mods: input.Mods = gtk_key.eventMods(
|
||||||
@ptrCast(self.gl_area),
|
|
||||||
event,
|
event,
|
||||||
physical_key,
|
physical_key,
|
||||||
gtk_mods,
|
gtk_mods,
|
||||||
if (self.app.x11_xkb) |*xkb| xkb else null,
|
&self.app.winproto,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get our consumed modifiers
|
// Get our consumed modifiers
|
||||||
|
@ -25,7 +25,7 @@ const gtk_key = @import("key.zig");
|
|||||||
const Notebook = @import("notebook.zig").Notebook;
|
const Notebook = @import("notebook.zig").Notebook;
|
||||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||||
const version = @import("version.zig");
|
const version = @import("version.zig");
|
||||||
const wayland = @import("wayland.zig");
|
const winproto = @import("winproto.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
@ -56,7 +56,8 @@ toast_overlay: ?*c.GtkWidget,
|
|||||||
/// See adwTabOverviewOpen for why we have this.
|
/// See adwTabOverviewOpen for why we have this.
|
||||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||||
|
|
||||||
wayland: ?wayland.SurfaceState,
|
/// State and logic for windowing protocol for a window.
|
||||||
|
winproto: ?winproto.Window,
|
||||||
|
|
||||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||||
// Allocate a fixed pointer for our window. We try to minimize
|
// Allocate a fixed pointer for our window. We try to minimize
|
||||||
@ -82,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
.notebook = undefined,
|
.notebook = undefined,
|
||||||
.context_menu = undefined,
|
.context_menu = undefined,
|
||||||
.toast_overlay = undefined,
|
.toast_overlay = undefined,
|
||||||
.wayland = null,
|
.winproto = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the window
|
// Create the window
|
||||||
@ -384,6 +385,16 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.gtk_widget_show(window);
|
c.gtk_widget_show(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn updateConfig(
|
||||||
|
self: *Window,
|
||||||
|
config: *const configpkg.Config,
|
||||||
|
) !void {
|
||||||
|
if (self.winproto) |*v| try v.updateConfigEvent(config);
|
||||||
|
|
||||||
|
// We always resync our appearance whenever the config changes.
|
||||||
|
try self.syncAppearance(config);
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates appearance based on config settings. Will be called once upon window
|
/// Updates appearance based on config settings. Will be called once upon window
|
||||||
/// realization, and every time the config is reloaded.
|
/// realization, and every time the config is reloaded.
|
||||||
///
|
///
|
||||||
@ -396,14 +407,10 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
|
|||||||
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
|
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.wayland) |*wl| {
|
// Window protocol specific appearance updates
|
||||||
const blurred = switch (config.@"background-blur-radius") {
|
if (self.winproto) |*v| v.syncAppearance() catch |err| {
|
||||||
.false => false,
|
log.warn("failed to sync window protocol appearance error={}", .{err});
|
||||||
.true => true,
|
};
|
||||||
.radius => |v| v > 0,
|
|
||||||
};
|
|
||||||
try wl.setBlur(blurred);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
||||||
@ -443,7 +450,7 @@ fn initActions(self: *Window) void {
|
|||||||
pub fn deinit(self: *Window) void {
|
pub fn deinit(self: *Window) void {
|
||||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
||||||
|
|
||||||
if (self.wayland) |*wl| wl.deinit();
|
if (self.winproto) |*v| v.deinit(self.app.core_app.alloc);
|
||||||
|
|
||||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||||
_ = c.g_source_remove(timer);
|
_ = c.g_source_remove(timer);
|
||||||
@ -584,10 +591,19 @@ pub fn sendToast(self: *Window, title: [:0]const u8) void {
|
|||||||
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||||
const self = userdataSelf(ud.?);
|
const self = userdataSelf(ud.?);
|
||||||
|
|
||||||
if (self.app.wayland) |*wl| {
|
// Initialize our window protocol logic
|
||||||
self.wayland = wayland.SurfaceState.init(v, wl);
|
if (winproto.Window.init(
|
||||||
|
self.app.core_app.alloc,
|
||||||
|
&self.app.winproto,
|
||||||
|
v,
|
||||||
|
&self.app.config,
|
||||||
|
)) |winproto_win| {
|
||||||
|
self.winproto = winproto_win;
|
||||||
|
} else |err| {
|
||||||
|
log.warn("failed to initialize window protocol error={}", .{err});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When we are realized we always setup our appearance
|
||||||
self.syncAppearance(&self.app.config) catch |err| {
|
self.syncAppearance(&self.app.config) catch |err| {
|
||||||
log.err("failed to initialize appearance={}", .{err});
|
log.err("failed to initialize appearance={}", .{err});
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,8 @@ pub const c = @cImport({
|
|||||||
// Add in X11-specific GDK backend which we use for specific things
|
// Add in X11-specific GDK backend which we use for specific things
|
||||||
// (e.g. X11 window class).
|
// (e.g. X11 window class).
|
||||||
@cInclude("gdk/x11/gdkx.h");
|
@cInclude("gdk/x11/gdkx.h");
|
||||||
|
@cInclude("X11/Xlib.h");
|
||||||
|
@cInclude("X11/Xatom.h");
|
||||||
// Xkb for X11 state handling
|
// Xkb for X11 state handling
|
||||||
@cInclude("X11/XKBlib.h");
|
@cInclude("X11/XKBlib.h");
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
|||||||
const build_options = @import("build_options");
|
const build_options = @import("build_options");
|
||||||
const input = @import("../../input.zig");
|
const input = @import("../../input.zig");
|
||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
const x11 = @import("x11.zig");
|
const winproto = @import("winproto.zig");
|
||||||
|
|
||||||
/// Returns a GTK accelerator string from a trigger.
|
/// Returns a GTK accelerator string from a trigger.
|
||||||
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||||
@ -105,34 +105,14 @@ pub fn keyvalUnicodeUnshifted(
|
|||||||
/// This requires a lot of context because the GdkEvent
|
/// This requires a lot of context because the GdkEvent
|
||||||
/// doesn't contain enough on its own.
|
/// doesn't contain enough on its own.
|
||||||
pub fn eventMods(
|
pub fn eventMods(
|
||||||
widget: *c.GtkWidget,
|
|
||||||
event: *c.GdkEvent,
|
event: *c.GdkEvent,
|
||||||
physical_key: input.Key,
|
physical_key: input.Key,
|
||||||
gtk_mods: c.GdkModifierType,
|
gtk_mods: c.GdkModifierType,
|
||||||
x11_xkb: ?*x11.Xkb,
|
app_winproto: *winproto.App,
|
||||||
) input.Mods {
|
) input.Mods {
|
||||||
const device = c.gdk_event_get_device(event);
|
const device = c.gdk_event_get_device(event);
|
||||||
|
|
||||||
var mods = mods: {
|
var mods = app_winproto.eventMods(device, gtk_mods);
|
||||||
// Add any modifier state events from Xkb if we have them (X11
|
|
||||||
// only). Null back from the Xkb call means there was no modifier
|
|
||||||
// event to read. This likely means that the key event did not
|
|
||||||
// result in a modifier change and we can safely rely on the GDK
|
|
||||||
// state.
|
|
||||||
if (comptime build_options.x11) {
|
|
||||||
const display = c.gtk_widget_get_display(widget);
|
|
||||||
if (x11_xkb) |xkb| {
|
|
||||||
if (xkb.modifier_state_from_notify(display)) |x11_mods| break :mods x11_mods;
|
|
||||||
break :mods translateMods(gtk_mods);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Wayland, we have to use the GDK device because the mods sent
|
|
||||||
// to this event do not have the modifier key applied if it was
|
|
||||||
// pressed (i.e. left control).
|
|
||||||
break :mods translateMods(c.gdk_device_get_modifier_state(device));
|
|
||||||
};
|
|
||||||
|
|
||||||
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
|
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
|
||||||
|
|
||||||
switch (physical_key) {
|
switch (physical_key) {
|
||||||
|
@ -1,125 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const c = @import("c.zig").c;
|
|
||||||
const wayland = @import("wayland");
|
|
||||||
const wl = wayland.client.wl;
|
|
||||||
const org = wayland.client.org;
|
|
||||||
const build_options = @import("build_options");
|
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_wayland);
|
|
||||||
|
|
||||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
|
||||||
pub const AppState = struct {
|
|
||||||
display: *wl.Display,
|
|
||||||
blur_manager: ?*org.KdeKwinBlurManager = null,
|
|
||||||
|
|
||||||
pub fn init(display: ?*c.GdkDisplay) ?AppState {
|
|
||||||
if (comptime !build_options.wayland) return null;
|
|
||||||
|
|
||||||
// It should really never be null
|
|
||||||
const display_ = display orelse return null;
|
|
||||||
|
|
||||||
// Check if we're actually on Wayland
|
|
||||||
if (c.g_type_check_instance_is_a(
|
|
||||||
@ptrCast(@alignCast(display_)),
|
|
||||||
c.gdk_wayland_display_get_type(),
|
|
||||||
) == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const wl_display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(display_) orelse return null);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.display = wl_display,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register(self: *AppState) !void {
|
|
||||||
const registry = try self.display.getRegistry();
|
|
||||||
|
|
||||||
registry.setListener(*AppState, registryListener, self);
|
|
||||||
if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
|
||||||
|
|
||||||
log.debug("app wayland init={}", .{self});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Wayland state that contains Wayland objects associated with a window (e.g. wl_surface).
|
|
||||||
pub const SurfaceState = struct {
|
|
||||||
app_state: *AppState,
|
|
||||||
surface: *wl.Surface,
|
|
||||||
|
|
||||||
/// A token that, when present, indicates that the window is blurred.
|
|
||||||
blur_token: ?*org.KdeKwinBlur = null,
|
|
||||||
|
|
||||||
pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState {
|
|
||||||
if (comptime !build_options.wayland) return null;
|
|
||||||
|
|
||||||
const surface = c.gtk_native_get_surface(@ptrCast(window)) orelse return null;
|
|
||||||
|
|
||||||
// Check if we're actually on Wayland
|
|
||||||
if (c.g_type_check_instance_is_a(
|
|
||||||
@ptrCast(@alignCast(surface)),
|
|
||||||
c.gdk_wayland_surface_get_type(),
|
|
||||||
) == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return null);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.app_state = app_state,
|
|
||||||
.surface = wl_surface,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *SurfaceState) void {
|
|
||||||
if (self.blur_token) |blur| blur.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setBlur(self: *SurfaceState, blurred: bool) !void {
|
|
||||||
log.debug("setting blur={}", .{blurred});
|
|
||||||
|
|
||||||
const mgr = self.app_state.blur_manager orelse {
|
|
||||||
log.warn("can't set blur: org_kde_kwin_blur_manager protocol unavailable", .{});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.blur_token) |blur| {
|
|
||||||
// Only release token when transitioning from blurred -> not blurred
|
|
||||||
if (!blurred) {
|
|
||||||
mgr.unset(self.surface);
|
|
||||||
blur.release();
|
|
||||||
self.blur_token = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only acquire token when transitioning from not blurred -> blurred
|
|
||||||
if (blurred) {
|
|
||||||
const blur_token = try mgr.create(self.surface);
|
|
||||||
blur_token.commit();
|
|
||||||
self.blur_token = blur_token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void {
|
|
||||||
switch (event) {
|
|
||||||
.global => |global| {
|
|
||||||
log.debug("got global interface={s}", .{global.interface});
|
|
||||||
if (bindInterface(org.KdeKwinBlurManager, registry, global, 1)) |iface| {
|
|
||||||
state.blur_manager = iface;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.global_remove => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bindInterface(comptime T: type, registry: *wl.Registry, global: anytype, version: u32) ?*T {
|
|
||||||
if (std.mem.orderZ(u8, global.interface, T.interface.name) == .eq) {
|
|
||||||
return registry.bind(global.name, T, version) catch |err| {
|
|
||||||
log.warn("encountered error={} while binding interface {s}", .{ err, global.interface });
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
128
src/apprt/gtk/winproto.zig
Normal file
128
src/apprt/gtk/winproto.zig
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const build_options = @import("build_options");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const c = @import("c.zig").c;
|
||||||
|
const Config = @import("../../config.zig").Config;
|
||||||
|
const input = @import("../../input.zig");
|
||||||
|
const key = @import("key.zig");
|
||||||
|
|
||||||
|
pub const noop = @import("winproto/noop.zig");
|
||||||
|
pub const x11 = @import("winproto/x11.zig");
|
||||||
|
pub const wayland = @import("winproto/wayland.zig");
|
||||||
|
|
||||||
|
pub const Protocol = enum {
|
||||||
|
none,
|
||||||
|
wayland,
|
||||||
|
x11,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// App-state for the underlying windowing protocol. There should be one
|
||||||
|
/// instance of this struct per application.
|
||||||
|
pub const App = union(Protocol) {
|
||||||
|
none: noop.App,
|
||||||
|
wayland: if (build_options.wayland) wayland.App else noop.App,
|
||||||
|
x11: if (build_options.x11) x11.App else noop.App,
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
alloc: Allocator,
|
||||||
|
gdk_display: *c.GdkDisplay,
|
||||||
|
app_id: [:0]const u8,
|
||||||
|
config: *const Config,
|
||||||
|
) !App {
|
||||||
|
inline for (@typeInfo(App).Union.fields) |field| {
|
||||||
|
if (try field.type.init(
|
||||||
|
alloc,
|
||||||
|
gdk_display,
|
||||||
|
app_id,
|
||||||
|
config,
|
||||||
|
)) |v| {
|
||||||
|
return @unionInit(App, field.name, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .none = .{} };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||||
|
switch (self.*) {
|
||||||
|
inline else => |*v| v.deinit(alloc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventMods(
|
||||||
|
self: *App,
|
||||||
|
device: ?*c.GdkDevice,
|
||||||
|
gtk_mods: c.GdkModifierType,
|
||||||
|
) input.Mods {
|
||||||
|
return switch (self.*) {
|
||||||
|
inline else => |*v| v.eventMods(device, gtk_mods),
|
||||||
|
} orelse key.translateMods(gtk_mods);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Per-Window state for the underlying windowing protocol.
|
||||||
|
///
|
||||||
|
/// In both X and Wayland, the terminology used is "Surface" and this is
|
||||||
|
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
|
||||||
|
/// heavily to mean something completely different, so we use "Window" here
|
||||||
|
/// to better match what it generally maps to in the Ghostty codebase.
|
||||||
|
pub const Window = union(Protocol) {
|
||||||
|
none: noop.Window,
|
||||||
|
wayland: if (build_options.wayland) wayland.Window else noop.Window,
|
||||||
|
x11: if (build_options.x11) x11.Window else noop.Window,
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
alloc: Allocator,
|
||||||
|
app: *App,
|
||||||
|
window: *c.GtkWindow,
|
||||||
|
config: *const Config,
|
||||||
|
) !Window {
|
||||||
|
return switch (app.*) {
|
||||||
|
inline else => |*v, tag| {
|
||||||
|
inline for (@typeInfo(Window).Union.fields) |field| {
|
||||||
|
if (comptime std.mem.eql(
|
||||||
|
u8,
|
||||||
|
field.name,
|
||||||
|
@tagName(tag),
|
||||||
|
)) return @unionInit(
|
||||||
|
Window,
|
||||||
|
field.name,
|
||||||
|
try field.type.init(
|
||||||
|
alloc,
|
||||||
|
v,
|
||||||
|
window,
|
||||||
|
config,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Window, alloc: Allocator) void {
|
||||||
|
switch (self.*) {
|
||||||
|
inline else => |*v| v.deinit(alloc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resizeEvent(self: *Window) !void {
|
||||||
|
switch (self.*) {
|
||||||
|
inline else => |*v| try v.resizeEvent(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateConfigEvent(
|
||||||
|
self: *Window,
|
||||||
|
config: *const Config,
|
||||||
|
) !void {
|
||||||
|
switch (self.*) {
|
||||||
|
inline else => |*v| try v.updateConfigEvent(config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn syncAppearance(self: *Window) !void {
|
||||||
|
switch (self.*) {
|
||||||
|
inline else => |*v| try v.syncAppearance(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
56
src/apprt/gtk/winproto/noop.zig
Normal file
56
src/apprt/gtk/winproto/noop.zig
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const c = @import("../c.zig").c;
|
||||||
|
const Config = @import("../../../config.zig").Config;
|
||||||
|
const input = @import("../../../input.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.winproto_noop);
|
||||||
|
|
||||||
|
pub const App = struct {
|
||||||
|
pub fn init(
|
||||||
|
_: Allocator,
|
||||||
|
_: *c.GdkDisplay,
|
||||||
|
_: [:0]const u8,
|
||||||
|
_: *const Config,
|
||||||
|
) !?App {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||||
|
_ = self;
|
||||||
|
_ = alloc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventMods(
|
||||||
|
_: *App,
|
||||||
|
_: ?*c.GdkDevice,
|
||||||
|
_: c.GdkModifierType,
|
||||||
|
) ?input.Mods {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Window = struct {
|
||||||
|
pub fn init(
|
||||||
|
_: Allocator,
|
||||||
|
_: *App,
|
||||||
|
_: *c.GtkWindow,
|
||||||
|
_: *const Config,
|
||||||
|
) !Window {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||||
|
_ = self;
|
||||||
|
_ = alloc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateConfigEvent(
|
||||||
|
_: *Window,
|
||||||
|
_: *const Config,
|
||||||
|
) !void {}
|
||||||
|
|
||||||
|
pub fn resizeEvent(_: *Window) !void {}
|
||||||
|
|
||||||
|
pub fn syncAppearance(_: *Window) !void {}
|
||||||
|
};
|
211
src/apprt/gtk/winproto/wayland.zig
Normal file
211
src/apprt/gtk/winproto/wayland.zig
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||||
|
const std = @import("std");
|
||||||
|
const wayland = @import("wayland");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const c = @import("../c.zig").c;
|
||||||
|
const Config = @import("../../../config.zig").Config;
|
||||||
|
const input = @import("../../../input.zig");
|
||||||
|
|
||||||
|
const wl = wayland.client.wl;
|
||||||
|
const org = wayland.client.org;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.winproto_wayland);
|
||||||
|
|
||||||
|
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||||
|
pub const App = struct {
|
||||||
|
display: *wl.Display,
|
||||||
|
context: *Context,
|
||||||
|
|
||||||
|
const Context = struct {
|
||||||
|
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
alloc: Allocator,
|
||||||
|
gdk_display: *c.GdkDisplay,
|
||||||
|
app_id: [:0]const u8,
|
||||||
|
config: *const Config,
|
||||||
|
) !?App {
|
||||||
|
_ = config;
|
||||||
|
_ = app_id;
|
||||||
|
|
||||||
|
// Check if we're actually on Wayland
|
||||||
|
if (c.g_type_check_instance_is_a(
|
||||||
|
@ptrCast(@alignCast(gdk_display)),
|
||||||
|
c.gdk_wayland_display_get_type(),
|
||||||
|
) == 0) return null;
|
||||||
|
|
||||||
|
const display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(
|
||||||
|
gdk_display,
|
||||||
|
) orelse return error.NoWaylandDisplay);
|
||||||
|
|
||||||
|
// Create our context for our callbacks so we have a stable pointer.
|
||||||
|
// Note: at the time of writing this comment, we don't really need
|
||||||
|
// 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 alloc.destroy(context);
|
||||||
|
context.* = .{};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.display = display,
|
||||||
|
.context = context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||||
|
alloc.destroy(self.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventMods(
|
||||||
|
_: *App,
|
||||||
|
_: ?*c.GdkDevice,
|
||||||
|
_: c.GdkModifierType,
|
||||||
|
) ?input.Mods {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registryListener(
|
||||||
|
registry: *wl.Registry,
|
||||||
|
event: wl.Registry.Event,
|
||||||
|
context: *Context,
|
||||||
|
) void {
|
||||||
|
switch (event) {
|
||||||
|
// https://wayland.app/protocols/wayland#wl_registry:event:global
|
||||||
|
.global => |global| global: {
|
||||||
|
log.debug("wl_registry.global: interface={s}", .{global.interface});
|
||||||
|
|
||||||
|
if (registryBind(
|
||||||
|
org.KdeKwinBlurManager,
|
||||||
|
registry,
|
||||||
|
global,
|
||||||
|
1,
|
||||||
|
)) |blur_manager| {
|
||||||
|
context.kde_blur_manager = blur_manager;
|
||||||
|
break :global;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// We don't handle removal events
|
||||||
|
.global_remove => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registryBind(
|
||||||
|
comptime T: type,
|
||||||
|
registry: *wl.Registry,
|
||||||
|
global: anytype,
|
||||||
|
version: u32,
|
||||||
|
) ?*T {
|
||||||
|
if (std.mem.orderZ(
|
||||||
|
u8,
|
||||||
|
global.interface,
|
||||||
|
T.interface.name,
|
||||||
|
) != .eq) return null;
|
||||||
|
|
||||||
|
return registry.bind(global.name, T, version) catch |err| {
|
||||||
|
log.warn("error binding interface {s} error={}", .{
|
||||||
|
global.interface,
|
||||||
|
err,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||||
|
pub const Window = struct {
|
||||||
|
config: DerivedConfig,
|
||||||
|
|
||||||
|
/// The Wayland surface for this window.
|
||||||
|
surface: *wl.Surface,
|
||||||
|
|
||||||
|
/// The context from the app where we can load our Wayland interfaces.
|
||||||
|
app_context: *App.Context,
|
||||||
|
|
||||||
|
/// A token that, when present, indicates that the window is blurred.
|
||||||
|
blur_token: ?*org.KdeKwinBlur = null,
|
||||||
|
|
||||||
|
const DerivedConfig = struct {
|
||||||
|
blur: bool,
|
||||||
|
|
||||||
|
pub fn init(config: *const Config) DerivedConfig {
|
||||||
|
return .{
|
||||||
|
.blur = config.@"background-blur-radius".enabled(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
alloc: Allocator,
|
||||||
|
app: *App,
|
||||||
|
gtk_window: *c.GtkWindow,
|
||||||
|
config: *const Config,
|
||||||
|
) !Window {
|
||||||
|
_ = alloc;
|
||||||
|
|
||||||
|
const gdk_surface = c.gtk_native_get_surface(
|
||||||
|
@ptrCast(gtk_window),
|
||||||
|
) orelse return error.NotWaylandSurface;
|
||||||
|
|
||||||
|
// This should never fail, because if we're being called at this point
|
||||||
|
// then we've already asserted that our app state is Wayland.
|
||||||
|
if (c.g_type_check_instance_is_a(
|
||||||
|
@ptrCast(@alignCast(gdk_surface)),
|
||||||
|
c.gdk_wayland_surface_get_type(),
|
||||||
|
) == 0) return error.NotWaylandSurface;
|
||||||
|
|
||||||
|
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(
|
||||||
|
gdk_surface,
|
||||||
|
) orelse return error.NoWaylandSurface);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.config = DerivedConfig.init(config),
|
||||||
|
.surface = wl_surface,
|
||||||
|
.app_context = app.context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||||
|
_ = alloc;
|
||||||
|
if (self.blur_token) |blur| blur.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateConfigEvent(self: *Window, config: *const Config) !void {
|
||||||
|
self.config = DerivedConfig.init(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resizeEvent(_: *Window) !void {}
|
||||||
|
|
||||||
|
pub fn syncAppearance(self: *Window) !void {
|
||||||
|
try self.syncBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the blur state of the window.
|
||||||
|
fn syncBlur(self: *Window) !void {
|
||||||
|
const manager = self.app_context.kde_blur_manager orelse return;
|
||||||
|
const blur = self.config.blur;
|
||||||
|
|
||||||
|
if (self.blur_token) |tok| {
|
||||||
|
// Only release token when transitioning from blurred -> not blurred
|
||||||
|
if (!blur) {
|
||||||
|
manager.unset(self.surface);
|
||||||
|
tok.release();
|
||||||
|
self.blur_token = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only acquire token when transitioning from not blurred -> blurred
|
||||||
|
if (blur) {
|
||||||
|
const tok = try manager.create(self.surface);
|
||||||
|
tok.commit();
|
||||||
|
self.blur_token = tok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
293
src/apprt/gtk/winproto/x11.zig
Normal file
293
src/apprt/gtk/winproto/x11.zig
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
//! 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,
|
||||||
|
kde_blur_atom: c.Atom,
|
||||||
|
|
||||||
|
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,
|
||||||
|
.kde_blur_atom = c.gdk_x11_get_xatom_by_name_for_display(
|
||||||
|
gdk_display,
|
||||||
|
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
config: DerivedConfig,
|
||||||
|
window: c.Window,
|
||||||
|
gtk_window: *c.GtkWindow,
|
||||||
|
blur_region: Region,
|
||||||
|
|
||||||
|
const DerivedConfig = struct {
|
||||||
|
blur: bool,
|
||||||
|
|
||||||
|
pub fn init(config: *const Config) DerivedConfig {
|
||||||
|
return .{
|
||||||
|
.blur = config.@"background-blur-radius".enabled(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
_: 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;
|
||||||
|
|
||||||
|
const blur_region: Region = blur: {
|
||||||
|
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
|
||||||
|
!adwaita.enabled(config)) break :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(gtk_window),
|
||||||
|
&x,
|
||||||
|
&y,
|
||||||
|
);
|
||||||
|
|
||||||
|
break :blur .{
|
||||||
|
.x = @intFromFloat(x),
|
||||||
|
.y = @intFromFloat(y),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.app = app,
|
||||||
|
.config = DerivedConfig.init(config),
|
||||||
|
.window = c.gdk_x11_surface_get_xid(surface),
|
||||||
|
.gtk_window = gtk_window,
|
||||||
|
.blur_region = blur_region,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
try self.syncBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
_ = c.XChangeProperty(
|
||||||
|
self.app.display,
|
||||||
|
self.window,
|
||||||
|
self.app.kde_blur_atom,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_ = c.XDeleteProperty(
|
||||||
|
self.app.display,
|
||||||
|
self.window,
|
||||||
|
self.app.kde_blur_atom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Region = extern struct {
|
||||||
|
x: c_long = 0,
|
||||||
|
y: c_long = 0,
|
||||||
|
width: c_long = 0,
|
||||||
|
height: c_long = 0,
|
||||||
|
};
|
@ -1,119 +0,0 @@
|
|||||||
/// Utility functions for X11 handling.
|
|
||||||
const std = @import("std");
|
|
||||||
const build_options = @import("build_options");
|
|
||||||
const c = @import("c.zig").c;
|
|
||||||
const input = @import("../../input.zig");
|
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_x11);
|
|
||||||
|
|
||||||
/// Returns true if the passed in display is an X11 display.
|
|
||||||
pub fn is_display(display: ?*c.GdkDisplay) bool {
|
|
||||||
if (comptime !build_options.x11) return false;
|
|
||||||
return c.g_type_check_instance_is_a(
|
|
||||||
@ptrCast(@alignCast(display orelse return false)),
|
|
||||||
c.gdk_x11_display_get_type(),
|
|
||||||
) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the app is running on X11
|
|
||||||
pub fn is_current_display_server() bool {
|
|
||||||
if (comptime !build_options.x11) return false;
|
|
||||||
const display = c.gdk_display_get_default();
|
|
||||||
return is_display(display);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Xkb = struct {
|
|
||||||
base_event_code: c_int,
|
|
||||||
|
|
||||||
/// Initialize an Xkb struct for the given GDK display. If the display isn't
|
|
||||||
/// backed by X then this will return null.
|
|
||||||
pub fn init(display_: ?*c.GdkDisplay) !?Xkb {
|
|
||||||
if (comptime !build_options.x11) return null;
|
|
||||||
|
|
||||||
// Display should never be null but we just treat that as a non-X11
|
|
||||||
// display so that the caller can just ignore it and not unwrap it.
|
|
||||||
const display = display_ orelse return null;
|
|
||||||
|
|
||||||
// If the display isn't X11, then we don't need to do anything.
|
|
||||||
if (!is_display(display)) return null;
|
|
||||||
|
|
||||||
log.debug("Xkb.init: initializing Xkb", .{});
|
|
||||||
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
|
|
||||||
var result: Xkb = .{
|
|
||||||
.base_event_code = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.debug("Xkb.init: running XkbQueryExtension", .{});
|
|
||||||
var opcode: c_int = 0;
|
|
||||||
var base_error_code: c_int = 0;
|
|
||||||
var major = c.XkbMajorVersion;
|
|
||||||
var minor = c.XkbMinorVersion;
|
|
||||||
if (c.XkbQueryExtension(
|
|
||||||
xdisplay,
|
|
||||||
&opcode,
|
|
||||||
&result.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(
|
|
||||||
xdisplay,
|
|
||||||
c.XkbUseCoreKbd,
|
|
||||||
c.XkbStateNotify,
|
|
||||||
c.XkbModifierStateMask,
|
|
||||||
c.XkbModifierStateMask,
|
|
||||||
) == 0) {
|
|
||||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
|
||||||
return error.XkbInitializationError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 modifier_state_from_notify(self: Xkb, display_: ?*c.GdkDisplay) ?input.Mods {
|
|
||||||
if (comptime !build_options.x11) return null;
|
|
||||||
|
|
||||||
const display = display_ orelse return null;
|
|
||||||
|
|
||||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
|
||||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
|
||||||
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
|
|
||||||
if (c.XEventsQueued(xdisplay, c.QueuedAfterReading) == 0) return null;
|
|
||||||
|
|
||||||
var nextEvent: c.XEvent = undefined;
|
|
||||||
_ = c.XPeekEvent(xdisplay, &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;
|
|
||||||
}
|
|
||||||
};
|
|
@ -604,7 +604,7 @@ palette: Palette = .{},
|
|||||||
///
|
///
|
||||||
/// Supported on macOS and on some Linux desktop environments, including:
|
/// Supported on macOS and on some Linux desktop environments, including:
|
||||||
///
|
///
|
||||||
/// * KDE Plasma (Wayland only)
|
/// * KDE Plasma (Wayland and X11)
|
||||||
///
|
///
|
||||||
/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting
|
/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting
|
||||||
/// this setting to either `true` or any positive blur intensity value would
|
/// this setting to either `true` or any positive blur intensity value would
|
||||||
@ -5782,6 +5782,14 @@ pub const BackgroundBlur = union(enum) {
|
|||||||
) catch return error.InvalidValue };
|
) catch return error.InvalidValue };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn enabled(self: BackgroundBlur) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.false => false,
|
||||||
|
.true => true,
|
||||||
|
.radius => |v| v > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cval(self: BackgroundBlur) u8 {
|
pub fn cval(self: BackgroundBlur) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.false => 0,
|
.false => 0,
|
||||||
|
Reference in New Issue
Block a user