Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gtk: unify Wayland and X11 platforms, implement background blur for KDE X11 #4723

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 4 additions & 49 deletions src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ const c = @import("c.zig").c;
const version = @import("version.zig");
const inspector = @import("inspector.zig");
const key = @import("key.zig");
const x11 = @import("x11.zig");
const wayland = @import("wayland.zig");
const protocol = @import("protocol.zig");
const testing = std.testing;

const log = std.log.scoped(.gtk);
Expand Down Expand Up @@ -71,11 +70,7 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
/// This is set to false when the main loop should exit.
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,
protocol: protocol.App,

/// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled
Expand Down Expand Up @@ -364,46 +359,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
return error.GtkApplicationRegisterFailed;
}

// Perform all X11 initialization. This ultimately returns the X11
// keyboard state but the block does more than that (i.e. setting up
// WM_CLASS).
const x11_xkb: ?x11.Xkb = x11_xkb: {
if (comptime !build_options.x11) break :x11_xkb null;
if (!x11.is_display(display)) break :x11_xkb null;

// 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.
//
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();
const app_protocol = try protocol.App.init(display, &config, app_id);

// 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
Expand All @@ -429,8 +385,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
.config = config,
.ctx = ctx,
.cursor_none = cursor_none,
.x11_xkb = x11_xkb,
.wayland = wl,
.protocol = app_protocol,
.single_instance = single_instance,
// 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
Expand Down
11 changes: 5 additions & 6 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const ResizeOverlay = @import("ResizeOverlay.zig");
const inspector = @import("inspector.zig");
const gtk_key = @import("key.zig");
const c = @import("c.zig").c;
const x11 = @import("x11.zig");

const log = std.log.scoped(.gtk_surface);

Expand Down Expand Up @@ -807,9 +806,6 @@ pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
c.g_object_get_property(@ptrCast(@alignCast(settings)), "gtk-xft-dpi", &value);
const gtk_xft_dpi = c.g_value_get_int(&value);

// As noted above gtk-xft-dpi is multiplied by 1024, so we divide by
// 1024, then divide by the default value (96) to derive a scale. Note
// gtk-xft-dpi can be fractional, so we use floating point math here.
const xft_dpi: f32 = @as(f32, @floatFromInt(gtk_xft_dpi)) / 1024;
pluiedev marked this conversation as resolved.
Show resolved Hide resolved
break :xft_scale xft_dpi / 96;
};
Expand Down Expand Up @@ -1328,6 +1324,10 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
return;
};

if (self.container.window()) |window| window.protocol.onResize() catch |err| {
log.warn("failed to notify X11/Wayland integration of resize={}", .{err});
};

self.resize_overlay.maybeShow();
}
}
Expand Down Expand Up @@ -1643,11 +1643,10 @@ pub fn keyEvent(

// Get our modifier for the event
const mods: input.Mods = gtk_key.eventMods(
@ptrCast(self.gl_area),
event,
physical_key,
gtk_mods,
if (self.app.x11_xkb) |*xkb| xkb else null,
&self.app.protocol,
);

// Get our consumed modifiers
Expand Down
22 changes: 7 additions & 15 deletions src/apprt/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar;
const version = @import("version.zig");
const wayland = @import("wayland.zig");
const protocol = @import("protocol.zig");

const log = std.log.scoped(.gtk);

Expand Down Expand Up @@ -56,7 +56,7 @@ toast_overlay: ?*c.GtkWidget,
/// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null,

wayland: ?wayland.SurfaceState,
protocol: protocol.Surface,

pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize
Expand All @@ -82,7 +82,7 @@ pub fn init(self: *Window, app: *App) !void {
.notebook = undefined,
.context_menu = undefined,
.toast_overlay = undefined,
.wayland = null,
.protocol = undefined,
};

// Create the window
Expand Down Expand Up @@ -396,14 +396,8 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
}

if (self.wayland) |*wl| {
const blurred = switch (config.@"background-blur-radius") {
.false => false,
.true => true,
.radius => |v| v > 0,
};
try wl.setBlur(blurred);
}
// Perform protocol-specific config updates
try self.protocol.onConfigUpdate(config);
}

/// Sets up the GTK actions for the window scope. Actions are how GTK handles
Expand Down Expand Up @@ -443,7 +437,7 @@ fn initActions(self: *Window) void {
pub fn deinit(self: *Window) void {
c.gtk_widget_unparent(@ptrCast(self.context_menu));

if (self.wayland) |*wl| wl.deinit();
self.protocol.deinit();

if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(timer);
Expand Down Expand Up @@ -568,9 +562,7 @@ pub fn sendToast(self: *Window, title: [:0]const u8) void {
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
const self = userdataSelf(ud.?);

if (self.app.wayland) |*wl| {
self.wayland = wayland.SurfaceState.init(v, wl);
}
self.protocol.init(v, &self.app.protocol, &self.app.config);

self.syncAppearance(&self.app.config) catch |err| {
log.err("failed to initialize appearance={}", .{err});
Expand Down
2 changes: 2 additions & 0 deletions src/apprt/gtk/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub const c = @cImport({
// Add in X11-specific GDK backend which we use for specific things
// (e.g. X11 window class).
@cInclude("gdk/x11/gdkx.h");
@cInclude("X11/Xlib.h");
@cInclude("X11/Xatom.h");
// Xkb for X11 state handling
@cInclude("X11/XKBlib.h");
}
Expand Down
26 changes: 3 additions & 23 deletions src/apprt/gtk/key.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const std = @import("std");
const build_options = @import("build_options");
const input = @import("../../input.zig");
const c = @import("c.zig").c;
const x11 = @import("x11.zig");
const protocol = @import("protocol.zig");

/// Returns a GTK accelerator string from a trigger.
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
Expand Down Expand Up @@ -105,34 +105,14 @@ pub fn keyvalUnicodeUnshifted(
/// This requires a lot of context because the GdkEvent
/// doesn't contain enough on its own.
pub fn eventMods(
widget: *c.GtkWidget,
event: *c.GdkEvent,
physical_key: input.Key,
gtk_mods: c.GdkModifierType,
x11_xkb: ?*x11.Xkb,
app_protocol: *protocol.App,
) input.Mods {
const device = c.gdk_event_get_device(event);

var mods = 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));
};

var mods = app_protocol.eventMods(device, gtk_mods);
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;

switch (physical_key) {
Expand Down
149 changes: 149 additions & 0 deletions src/apprt/gtk/protocol.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
const std = @import("std");
const x11 = @import("protocol/x11.zig");
const wayland = @import("protocol/wayland.zig");
const c = @import("c.zig").c;
const build_options = @import("build_options");
const input = @import("../../input.zig");
const apprt = @import("../../apprt.zig");
const Config = @import("../../config.zig").Config;
const adwaita = @import("adwaita.zig");
const builtin = @import("builtin");
const key = @import("key.zig");

const log = std.log.scoped(.gtk_platform);

pub const App = struct {
gdk_display: *c.GdkDisplay,
derived_config: DerivedConfig,

inner: union(enum) {
none,
x11: if (build_options.x11) x11.App else void,
wayland: if (build_options.wayland) wayland.App else void,
},

const DerivedConfig = struct {
app_id: [:0]const u8,
x11_program_name: [:0]const u8,

pub fn init(config: *const Config, app_id: [:0]const u8) DerivedConfig {
return .{
.app_id = app_id,
.x11_program_name = if (config.@"x11-instance-name") |pn|
pn
else if (builtin.mode == .Debug)
"ghostty-debug"
else
"ghostty",
};
}
};

pub fn init(display: ?*c.GdkDisplay, config: *const Config, app_id: [:0]const u8) !App {
var self: App = .{
.inner = .none,
.derived_config = DerivedConfig.init(config, app_id),
.gdk_display = display orelse {
// TODO: When does this ever happen...?
std.debug.panic("GDK display is null!", .{});
},
};

// The X11/Wayland init functions set `self.inner` when successful,
// so we only need to keep trying if `self.inner` stays `.none`
if (self.inner == .none and comptime build_options.wayland) try wayland.App.init(&self);
if (self.inner == .none and comptime build_options.x11) try x11.App.init(&self);

// Welp, no integration for you
if (self.inner == .none) {
log.warn(
"neither X11 nor Wayland integrations enabled - lots of features would be missing!",
.{},
);
}

return self;
}

pub fn eventMods(self: *App, device: ?*c.GdkDevice, gtk_mods: c.GdkModifierType) input.Mods {
return switch (self.inner) {
// 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.
.x11 => |*x| if (comptime build_options.x11)
x.modifierStateFromNotify() orelse key.translateMods(gtk_mods)
else
unreachable,

// 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).
.wayland, .none => key.translateMods(c.gdk_device_get_modifier_state(device)),
};
}
};

pub const Surface = struct {
app: *App,
gtk_window: *c.GtkWindow,
derived_config: DerivedConfig,

inner: union(enum) {
none,
x11: if (build_options.x11) x11.Surface else void,
wayland: if (build_options.wayland) wayland.Surface else void,
},

pub const DerivedConfig = struct {
blur: Config.BackgroundBlur,
adw_enabled: bool,

pub fn init(config: *const Config) DerivedConfig {
return .{
.blur = config.@"background-blur-radius",
.adw_enabled = adwaita.enabled(config),
};
}
};

pub fn init(self: *Surface, window: *c.GtkWindow, app: *App, config: *const Config) void {
self.* = .{
.app = app,
.derived_config = DerivedConfig.init(config),
.gtk_window = window,
.inner = .none,
};

switch (app.inner) {
.x11 => if (comptime build_options.x11) x11.Surface.init(self) else unreachable,
.wayland => if (comptime build_options.wayland) wayland.Surface.init(self) else unreachable,
.none => {},
}
}

pub fn deinit(self: Surface) void {
switch (self.inner) {
.wayland => |wl| if (comptime build_options.wayland) wl.deinit() else unreachable,
.x11, .none => {},
}
}

pub fn onConfigUpdate(self: *Surface, config: *const Config) !void {
self.derived_config = DerivedConfig.init(config);

switch (self.inner) {
.x11 => |*x| if (comptime build_options.x11) try x.onConfigUpdate() else unreachable,
.wayland => |*wl| if (comptime build_options.wayland) try wl.onConfigUpdate() else unreachable,
.none => {},
}
}

pub fn onResize(self: *Surface) !void {
switch (self.inner) {
.x11 => |*x| if (comptime build_options.x11) try x.onResize() else unreachable,
.wayland, .none => {},
}
}
};
Loading
Loading