diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b8e79959..1e021af64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -342,7 +342,8 @@ jobs: matrix: adwaita: ["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 needs: test env: @@ -374,7 +375,8 @@ jobs: zig build \ -Dapp-runtime=gtk \ -Dgtk-adwaita=${{ matrix.adwaita }} \ - -Dgtk-x11=${{ matrix.x11 }} + -Dgtk-x11=${{ matrix.x11 }} \ + -Dgtk-wayland=${{ matrix.wayland }} test-sentry-linux: strategy: diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index fa5eb7b9f..6fa98a011 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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 winproto = @import("winproto.zig"); const testing = std.testing; const log = std.log.scoped(.gtk); @@ -50,6 +49,9 @@ config: Config, app: *c.GtkApplication, ctx: *c.GMainContext, +/// State and logic for the underlying windowing protocol. +winproto: winproto.App, + /// True if the app was launched with single instance mode. single_instance: bool, @@ -71,12 +73,6 @@ 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, - /// The base path of the transient cgroup used to put all surfaces /// into their own cgroup. This is only set if cgroups are enabled /// and initialization was successful. @@ -166,7 +162,12 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { } 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 (adwaita.enabled(&config)) { @@ -364,46 +365,15 @@ 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(); + // Setup our windowing protocol logic + var winproto_app = try winproto.App.init( + core_app.alloc, + display, + app_id, + &config, + ); + errdefer winproto_app.deinit(core_app.alloc); + log.debug("windowing protocol={s}", .{@tagName(winproto_app)}); // 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 @@ -429,8 +399,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .config = config, .ctx = ctx, .cursor_none = cursor_none, - .x11_xkb = x11_xkb, - .wayland = wl, + .winproto = winproto_app, .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 @@ -458,6 +427,8 @@ pub fn terminate(self: *App) void { } self.custom_css_providers.deinit(self.core_app.alloc); + self.winproto.deinit(self.core_app.alloc); + self.config.deinit(); } @@ -882,9 +853,10 @@ fn configChange( new_config: *const Config, ) void { switch (target) { - .surface => |surface| { - if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| { - log.warn("error syncing appearance changes to window err={}", .{err}); + .surface => |surface| surface: { + const window = surface.rt_surface.container.window() orelse break :surface; + window.updateConfig(new_config) catch |err| { + log.warn("error updating config for window err={}", .{err}); }; }, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 35932ac5e..c16f696b1 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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); @@ -1384,6 +1383,14 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) 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(); } } @@ -1699,11 +1706,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.winproto, ); // Get our consumed modifiers diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 0f44cee7b..d0e678057 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -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 winproto = @import("winproto.zig"); const log = std.log.scoped(.gtk); @@ -56,7 +56,8 @@ toast_overlay: ?*c.GtkWidget, /// See adwTabOverviewOpen for why we have this. 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 { // 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, .context_menu = undefined, .toast_overlay = undefined, - .wayland = null, + .winproto = null, }; // Create the window @@ -384,6 +385,16 @@ pub fn init(self: *Window, app: *App) !void { 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 /// 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"); } - if (self.wayland) |*wl| { - const blurred = switch (config.@"background-blur-radius") { - .false => false, - .true => true, - .radius => |v| v > 0, - }; - try wl.setBlur(blurred); - } + // Window protocol specific appearance updates + if (self.winproto) |*v| v.syncAppearance() catch |err| { + log.warn("failed to sync window protocol appearance error={}", .{err}); + }; } /// 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 { 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| { _ = 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 { const self = userdataSelf(ud.?); - if (self.app.wayland) |*wl| { - self.wayland = wayland.SurfaceState.init(v, wl); + // Initialize our window protocol logic + 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| { log.err("failed to initialize appearance={}", .{err}); }; diff --git a/src/apprt/gtk/c.zig b/src/apprt/gtk/c.zig index dde99c78e..4dc8ea57f 100644 --- a/src/apprt/gtk/c.zig +++ b/src/apprt/gtk/c.zig @@ -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"); } diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 311bff0da..40c9ca9a4 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -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 winproto = @import("winproto.zig"); /// Returns a GTK accelerator string from a trigger. 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 /// 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_winproto: *winproto.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_winproto.eventMods(device, gtk_mods); mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1; switch (physical_key) { diff --git a/src/apprt/gtk/wayland.zig b/src/apprt/gtk/wayland.zig deleted file mode 100644 index 92446cc46..000000000 --- a/src/apprt/gtk/wayland.zig +++ /dev/null @@ -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; - } -} diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig new file mode 100644 index 000000000..cb873fe01 --- /dev/null +++ b/src/apprt/gtk/winproto.zig @@ -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(), + } + } +}; diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig new file mode 100644 index 000000000..14f3dc6a7 --- /dev/null +++ b/src/apprt/gtk/winproto/noop.zig @@ -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 {} +}; diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig new file mode 100644 index 000000000..3f7ad0068 --- /dev/null +++ b/src/apprt/gtk/winproto/wayland.zig @@ -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; + } + } + } +}; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig new file mode 100644 index 000000000..4eac9cdf3 --- /dev/null +++ b/src/apprt/gtk/winproto/x11.zig @@ -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, +}; diff --git a/src/apprt/gtk/x11.zig b/src/apprt/gtk/x11.zig deleted file mode 100644 index 21ff87b34..000000000 --- a/src/apprt/gtk/x11.zig +++ /dev/null @@ -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; - } -}; diff --git a/src/config/Config.zig b/src/config/Config.zig index eae6541be..7751f3e77 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -604,7 +604,7 @@ palette: Palette = .{}, /// /// 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 /// 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 }; } + pub fn enabled(self: BackgroundBlur) bool { + return switch (self) { + .false => false, + .true => true, + .radius => |v| v > 0, + }; + } + pub fn cval(self: BackgroundBlur) u8 { return switch (self) { .false => 0,