mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
gtk(wayland): add support for background blur on KDE Plasma (#4403)
Also establishes a foundation for Wayland support and fixes a minor bug (GTK windows remaining opaque when `background-opacity` is set to 1 on startup and later updated to less than 1 with a config reload) Can't update the Zig cache hash myself since I'm currently in China and my proxy's broken for some reason :( See also #4361, part of #4626
This commit is contained in:
57
build.zig
57
build.zig
@ -24,6 +24,8 @@ const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig");
|
|||||||
const Version = @import("src/build/Version.zig");
|
const Version = @import("src/build/Version.zig");
|
||||||
const Command = @import("src/Command.zig");
|
const Command = @import("src/Command.zig");
|
||||||
|
|
||||||
|
const Scanner = @import("zig_wayland").Scanner;
|
||||||
|
|
||||||
comptime {
|
comptime {
|
||||||
// This is the required Zig version for building this project. We allow
|
// This is the required Zig version for building this project. We allow
|
||||||
// any patch version but the major and minor must match exactly.
|
// any patch version but the major and minor must match exactly.
|
||||||
@ -105,19 +107,19 @@ pub fn build(b: *std.Build) !void {
|
|||||||
"Enables the use of Adwaita when using the GTK rendering backend.",
|
"Enables the use of Adwaita when using the GTK rendering backend.",
|
||||||
) orelse true;
|
) orelse true;
|
||||||
|
|
||||||
config.x11 = b.option(
|
var x11 = false;
|
||||||
bool,
|
var wayland = false;
|
||||||
"gtk-x11",
|
|
||||||
"Enables linking against X11 libraries when using the GTK rendering backend.",
|
|
||||||
) orelse x11: {
|
|
||||||
if (target.result.os.tag != .linux) break :x11 false;
|
|
||||||
|
|
||||||
|
if (target.result.os.tag == .linux) pkgconfig: {
|
||||||
var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator);
|
var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator);
|
||||||
|
|
||||||
pkgconfig.stdout_behavior = .Pipe;
|
pkgconfig.stdout_behavior = .Pipe;
|
||||||
pkgconfig.stderr_behavior = .Pipe;
|
pkgconfig.stderr_behavior = .Pipe;
|
||||||
|
|
||||||
try pkgconfig.spawn();
|
pkgconfig.spawn() catch |err| {
|
||||||
|
std.log.warn("failed to spawn pkg-config - disabling X11 and Wayland integrations: {}", .{err});
|
||||||
|
break :pkgconfig;
|
||||||
|
};
|
||||||
|
|
||||||
const output_max_size = 50 * 1024;
|
const output_max_size = 50 * 1024;
|
||||||
|
|
||||||
@ -139,18 +141,31 @@ pub fn build(b: *std.Build) !void {
|
|||||||
switch (term) {
|
switch (term) {
|
||||||
.Exited => |code| {
|
.Exited => |code| {
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| break :x11 true;
|
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| x11 = true;
|
||||||
break :x11 false;
|
if (std.mem.indexOf(u8, stdout.items, "wayland")) |_| wayland = true;
|
||||||
|
} else {
|
||||||
|
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
||||||
|
return error.Unexpected;
|
||||||
}
|
}
|
||||||
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
|
||||||
break :x11 false;
|
|
||||||
},
|
},
|
||||||
inline else => |code| {
|
inline else => |code| {
|
||||||
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
|
||||||
return error.Unexpected;
|
return error.Unexpected;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
config.x11 = b.option(
|
||||||
|
bool,
|
||||||
|
"gtk-x11",
|
||||||
|
"Enables linking against X11 libraries when using the GTK rendering backend.",
|
||||||
|
) orelse x11;
|
||||||
|
|
||||||
|
config.wayland = b.option(
|
||||||
|
bool,
|
||||||
|
"gtk-wayland",
|
||||||
|
"Enables linking against Wayland libraries when using the GTK rendering backend.",
|
||||||
|
) orelse wayland;
|
||||||
|
|
||||||
config.sentry = b.option(
|
config.sentry = b.option(
|
||||||
bool,
|
bool,
|
||||||
@ -1459,6 +1474,24 @@ fn addDeps(
|
|||||||
if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
|
if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
|
||||||
if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
|
if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
|
||||||
|
|
||||||
|
if (config.wayland) {
|
||||||
|
const scanner = Scanner.create(b, .{});
|
||||||
|
|
||||||
|
const wayland = b.createModule(.{ .root_source_file = scanner.result });
|
||||||
|
|
||||||
|
const plasma_wayland_protocols = b.dependency("plasma_wayland_protocols", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
|
||||||
|
|
||||||
|
scanner.generate("wl_compositor", 1);
|
||||||
|
scanner.generate("org_kde_kwin_blur_manager", 1);
|
||||||
|
|
||||||
|
step.root_module.addImport("wayland", wayland);
|
||||||
|
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const gresource = @import("src/apprt/gtk/gresource.zig");
|
const gresource = @import("src/apprt/gtk/gresource.zig");
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@
|
|||||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||||
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
||||||
},
|
},
|
||||||
|
.zig_wayland = .{
|
||||||
|
.url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
|
||||||
|
.hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
|
||||||
|
},
|
||||||
|
|
||||||
// C libs
|
// C libs
|
||||||
.cimgui = .{ .path = "./pkg/cimgui" },
|
.cimgui = .{ .path = "./pkg/cimgui" },
|
||||||
@ -64,5 +68,9 @@
|
|||||||
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
||||||
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
||||||
},
|
},
|
||||||
|
.plasma_wayland_protocols = .{
|
||||||
|
.url = "git+https://invent.kde.org/libraries/plasma-wayland-protocols.git?ref=master#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
|
||||||
|
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,9 @@
|
|||||||
pandoc,
|
pandoc,
|
||||||
hyperfine,
|
hyperfine,
|
||||||
typos,
|
typos,
|
||||||
|
wayland,
|
||||||
|
wayland-scanner,
|
||||||
|
wayland-protocols,
|
||||||
}: let
|
}: let
|
||||||
# See package.nix. Keep in sync.
|
# See package.nix. Keep in sync.
|
||||||
rpathLibs =
|
rpathLibs =
|
||||||
@ -80,6 +83,7 @@
|
|||||||
libadwaita
|
libadwaita
|
||||||
gtk4
|
gtk4
|
||||||
glib
|
glib
|
||||||
|
wayland
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
mkShell {
|
mkShell {
|
||||||
@ -153,6 +157,9 @@ in
|
|||||||
libadwaita
|
libadwaita
|
||||||
gtk4
|
gtk4
|
||||||
glib
|
glib
|
||||||
|
wayland
|
||||||
|
wayland-scanner
|
||||||
|
wayland-protocols
|
||||||
];
|
];
|
||||||
|
|
||||||
# This should be set onto the rpath of the ghostty binary if you want
|
# This should be set onto the rpath of the ghostty binary if you want
|
||||||
|
@ -10,10 +10,6 @@
|
|||||||
oniguruma,
|
oniguruma,
|
||||||
zlib,
|
zlib,
|
||||||
libGL,
|
libGL,
|
||||||
libX11,
|
|
||||||
libXcursor,
|
|
||||||
libXi,
|
|
||||||
libXrandr,
|
|
||||||
glib,
|
glib,
|
||||||
gtk4,
|
gtk4,
|
||||||
libadwaita,
|
libadwaita,
|
||||||
@ -26,7 +22,15 @@
|
|||||||
pandoc,
|
pandoc,
|
||||||
revision ? "dirty",
|
revision ? "dirty",
|
||||||
optimize ? "Debug",
|
optimize ? "Debug",
|
||||||
x11 ? true,
|
enableX11 ? true,
|
||||||
|
libX11,
|
||||||
|
libXcursor,
|
||||||
|
libXi,
|
||||||
|
libXrandr,
|
||||||
|
enableWayland ? true,
|
||||||
|
wayland,
|
||||||
|
wayland-protocols,
|
||||||
|
wayland-scanner,
|
||||||
}: let
|
}: let
|
||||||
# The Zig hook has no way to select the release type without actual
|
# The Zig hook has no way to select the release type without actual
|
||||||
# overriding of the default flags.
|
# overriding of the default flags.
|
||||||
@ -114,14 +118,19 @@ in
|
|||||||
version = "1.0.2";
|
version = "1.0.2";
|
||||||
inherit src;
|
inherit src;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs =
|
||||||
git
|
[
|
||||||
ncurses
|
git
|
||||||
pandoc
|
ncurses
|
||||||
pkg-config
|
pandoc
|
||||||
zig_hook
|
pkg-config
|
||||||
wrapGAppsHook4
|
zig_hook
|
||||||
];
|
wrapGAppsHook4
|
||||||
|
]
|
||||||
|
++ lib.optionals enableWayland [
|
||||||
|
wayland-scanner
|
||||||
|
wayland-protocols
|
||||||
|
];
|
||||||
|
|
||||||
buildInputs =
|
buildInputs =
|
||||||
[
|
[
|
||||||
@ -142,16 +151,19 @@ in
|
|||||||
glib
|
glib
|
||||||
gsettings-desktop-schemas
|
gsettings-desktop-schemas
|
||||||
]
|
]
|
||||||
++ lib.optionals x11 [
|
++ lib.optionals enableX11 [
|
||||||
libX11
|
libX11
|
||||||
libXcursor
|
libXcursor
|
||||||
libXi
|
libXi
|
||||||
libXrandr
|
libXrandr
|
||||||
|
]
|
||||||
|
++ lib.optionals enableWayland [
|
||||||
|
wayland
|
||||||
];
|
];
|
||||||
|
|
||||||
dontConfigure = true;
|
dontConfigure = true;
|
||||||
|
|
||||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString x11}";
|
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
|
||||||
|
|
||||||
preBuild = ''
|
preBuild = ''
|
||||||
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||||
# more details.
|
# more details.
|
||||||
"sha256-l+tZVL18qhm8BoBsQVbKfYmXQVObD0QMzQe6VBM/8Oo="
|
"sha256-eUY6MS3//r6pA/w9b+E4+YqmqUbzpUfL3afJJlnMhLY="
|
||||||
|
@ -1953,7 +1953,7 @@ pub const CAPI = struct {
|
|||||||
_ = CGSSetWindowBackgroundBlurRadius(
|
_ = CGSSetWindowBackgroundBlurRadius(
|
||||||
CGSDefaultConnectionForThread(),
|
CGSDefaultConnectionForThread(),
|
||||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||||
@intCast(config.@"background-blur-radius"),
|
@intCast(config.@"background-blur-radius".cval()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ 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 x11 = @import("x11.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);
|
||||||
@ -73,6 +74,9 @@ running: bool = true,
|
|||||||
/// Xkb state (X11 only). Will be null on Wayland.
|
/// Xkb state (X11 only). Will be null on Wayland.
|
||||||
x11_xkb: ?x11.Xkb = null,
|
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.
|
||||||
@ -397,6 +401,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
break :x11_xkb try x11.Xkb.init(display);
|
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
|
||||||
// for launching Ghostty in the "background" without immediately opening
|
// for launching Ghostty in the "background" without immediately opening
|
||||||
@ -422,6 +430,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.cursor_none = cursor_none,
|
.cursor_none = cursor_none,
|
||||||
.x11_xkb = x11_xkb,
|
.x11_xkb = x11_xkb,
|
||||||
|
.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
|
||||||
@ -838,9 +847,11 @@ fn configChange(
|
|||||||
new_config: *const Config,
|
new_config: *const Config,
|
||||||
) void {
|
) void {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
// We don't do anything for surface config change events. There
|
.surface => |surface| {
|
||||||
// is nothing to sync with regards to a surface today.
|
if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| {
|
||||||
.surface => {},
|
log.warn("error syncing appearance changes to window err={}", .{err});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
.app => {
|
.app => {
|
||||||
// We clone (to take ownership) and update our configuration.
|
// We clone (to take ownership) and update our configuration.
|
||||||
|
@ -25,6 +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 log = std.log.scoped(.gtk);
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
@ -55,6 +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,
|
||||||
|
|
||||||
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
|
||||||
// allocations but windows and other GUI requirements are so minimal
|
// allocations but windows and other GUI requirements are so minimal
|
||||||
@ -79,6 +82,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,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the window
|
// Create the window
|
||||||
@ -115,11 +119,6 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
|
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the window's background if any of the widgets need to be transparent
|
|
||||||
if (app.config.@"background-opacity" < 1) {
|
|
||||||
c.gtk_widget_remove_css_class(@ptrCast(window), "background");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create our box which will hold our widgets in the main content area.
|
// Create our box which will hold our widgets in the main content area.
|
||||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
|
||||||
@ -290,6 +289,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
|
|
||||||
// All of our events
|
// All of our events
|
||||||
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
|
||||||
@ -387,6 +387,28 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.gtk_widget_show(window);
|
c.gtk_widget_show(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates appearance based on config settings. Will be called once upon window
|
||||||
|
/// realization, and every time the config is reloaded.
|
||||||
|
///
|
||||||
|
/// TODO: Many of the initial style settings in `create` could possibly be made
|
||||||
|
/// reactive by moving them here.
|
||||||
|
pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
|
||||||
|
if (config.@"background-opacity" < 1) {
|
||||||
|
c.gtk_widget_remove_css_class(@ptrCast(self.window), "background");
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
/// menus and such. The menu is defined in App.zig but the action is defined
|
/// menus and such. The menu is defined in App.zig but the action is defined
|
||||||
/// here. The string name binds them.
|
/// here. The string name binds them.
|
||||||
@ -424,6 +446,8 @@ 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.adw_tab_overview_focus_timer) |timer| {
|
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||||
_ = c.g_source_remove(timer);
|
_ = c.g_source_remove(timer);
|
||||||
}
|
}
|
||||||
@ -551,6 +575,20 @@ pub fn sendToast(self: *Window, title: [:0]const u8) void {
|
|||||||
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
|
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.syncAppearance(&self.app.config) catch |err| {
|
||||||
|
log.err("failed to initialize appearance={}", .{err});
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
||||||
// sends an undefined value.
|
// sends an undefined value.
|
||||||
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
@ -14,6 +14,9 @@ pub const c = @cImport({
|
|||||||
// Xkb for X11 state handling
|
// Xkb for X11 state handling
|
||||||
@cInclude("X11/XKBlib.h");
|
@cInclude("X11/XKBlib.h");
|
||||||
}
|
}
|
||||||
|
if (build_options.wayland) {
|
||||||
|
@cInclude("gdk/wayland/gdkwayland.h");
|
||||||
|
}
|
||||||
|
|
||||||
// generated header files
|
// generated header files
|
||||||
@cInclude("ghostty_resources.h");
|
@cInclude("ghostty_resources.h");
|
||||||
|
125
src/apprt/gtk/wayland.zig
Normal file
125
src/apprt/gtk/wayland.zig
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ pub const BuildConfig = struct {
|
|||||||
flatpak: bool = false,
|
flatpak: bool = false,
|
||||||
adwaita: bool = false,
|
adwaita: bool = false,
|
||||||
x11: bool = false,
|
x11: bool = false,
|
||||||
|
wayland: bool = false,
|
||||||
sentry: bool = true,
|
sentry: bool = true,
|
||||||
app_runtime: apprt.Runtime = .none,
|
app_runtime: apprt.Runtime = .none,
|
||||||
renderer: rendererpkg.Impl = .opengl,
|
renderer: rendererpkg.Impl = .opengl,
|
||||||
@ -44,6 +45,7 @@ pub const BuildConfig = struct {
|
|||||||
step.addOption(bool, "flatpak", self.flatpak);
|
step.addOption(bool, "flatpak", self.flatpak);
|
||||||
step.addOption(bool, "adwaita", self.adwaita);
|
step.addOption(bool, "adwaita", self.adwaita);
|
||||||
step.addOption(bool, "x11", self.x11);
|
step.addOption(bool, "x11", self.x11);
|
||||||
|
step.addOption(bool, "wayland", self.wayland);
|
||||||
step.addOption(bool, "sentry", self.sentry);
|
step.addOption(bool, "sentry", self.sentry);
|
||||||
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
|
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
|
||||||
step.addOption(font.Backend, "font_backend", self.font_backend);
|
step.addOption(font.Backend, "font_backend", self.font_backend);
|
||||||
|
@ -533,7 +533,7 @@ fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseBool(v: []const u8) !bool {
|
pub fn parseBool(v: []const u8) !bool {
|
||||||
const t = &[_][]const u8{ "1", "t", "T", "true" };
|
const t = &[_][]const u8{ "1", "t", "T", "true" };
|
||||||
const f = &[_][]const u8{ "0", "f", "F", "false" };
|
const f = &[_][]const u8{ "0", "f", "F", "false" };
|
||||||
|
|
||||||
|
@ -68,6 +68,14 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
} else {
|
} else {
|
||||||
try stdout.print(" - libX11 : disabled\n", .{});
|
try stdout.print(" - libX11 : disabled\n", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We say `libwayland` since it is possible to build Ghostty without
|
||||||
|
// Wayland integration but with Wayland-enabled GTK
|
||||||
|
if (comptime build_options.wayland) {
|
||||||
|
try stdout.print(" - libwayland : enabled\n", .{});
|
||||||
|
} else {
|
||||||
|
try stdout.print(" - libwayland : disabled\n", .{});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -592,13 +592,38 @@ palette: Palette = .{},
|
|||||||
/// On macOS, changing this configuration requires restarting Ghostty completely.
|
/// On macOS, changing this configuration requires restarting Ghostty completely.
|
||||||
@"background-opacity": f64 = 1.0,
|
@"background-opacity": f64 = 1.0,
|
||||||
|
|
||||||
/// A positive value enables blurring of the background when background-opacity
|
/// Whether to blur the background when `background-opacity` is less than 1.
|
||||||
/// is less than 1. The value is the blur radius to apply. A value of 20
|
|
||||||
/// is reasonable for a good looking blur. Higher values will cause strange
|
|
||||||
/// rendering issues as well as performance issues.
|
|
||||||
///
|
///
|
||||||
/// This is only supported on macOS.
|
/// Valid values are:
|
||||||
@"background-blur-radius": u8 = 0,
|
///
|
||||||
|
/// * a nonnegative integer specifying the *blur intensity*
|
||||||
|
/// * `false`, equivalent to a blur intensity of 0
|
||||||
|
/// * `true`, equivalent to the default blur intensity of 20, which is
|
||||||
|
/// reasonable for a good looking blur. Higher blur intensities may
|
||||||
|
/// cause strange rendering and performance issues.
|
||||||
|
///
|
||||||
|
/// Supported on macOS and on some Linux desktop environments, including:
|
||||||
|
///
|
||||||
|
/// * KDE Plasma (Wayland only)
|
||||||
|
///
|
||||||
|
/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting
|
||||||
|
/// this setting to either `true` or any positive blur intensity value would
|
||||||
|
/// achieve the same effect. The reason is that KWin, the window compositor
|
||||||
|
/// powering Plasma, only has one global blur setting and does not allow
|
||||||
|
/// applications to specify individual blur settings.
|
||||||
|
///
|
||||||
|
/// To configure KWin's global blur setting, open System Settings and go to
|
||||||
|
/// "Apps & Windows" > "Window Management" > "Desktop Effects" and select the
|
||||||
|
/// "Blur" plugin. If disabled, enable it by ticking the checkbox to the left.
|
||||||
|
/// Then click on the "Configure" button and there will be two sliders that
|
||||||
|
/// allow you to set background blur and noise intensities for all apps,
|
||||||
|
/// including Ghostty.
|
||||||
|
///
|
||||||
|
/// All other Linux desktop environments are as of now unsupported. Users may
|
||||||
|
/// need to set environment-specific settings and/or install third-party plugins
|
||||||
|
/// in order to support background blur, as there isn't a unified interface for
|
||||||
|
/// doing so.
|
||||||
|
@"background-blur-radius": BackgroundBlur = .false,
|
||||||
|
|
||||||
/// The opacity level (opposite of transparency) of an unfocused split.
|
/// The opacity level (opposite of transparency) of an unfocused split.
|
||||||
/// Unfocused splits by default are slightly faded out to make it easier to see
|
/// Unfocused splits by default are slightly faded out to make it easier to see
|
||||||
@ -5647,6 +5672,70 @@ pub const AutoUpdate = enum {
|
|||||||
download,
|
download,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// See background-blur-radius
|
||||||
|
pub const BackgroundBlur = union(enum) {
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
radius: u8,
|
||||||
|
|
||||||
|
pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void {
|
||||||
|
const input_ = input orelse {
|
||||||
|
// Emulate behavior for bools
|
||||||
|
self.* = .true;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.* = if (cli.args.parseBool(input_)) |b|
|
||||||
|
if (b) .true else .false
|
||||||
|
else |_|
|
||||||
|
.{ .radius = std.fmt.parseInt(
|
||||||
|
u8,
|
||||||
|
input_,
|
||||||
|
0,
|
||||||
|
) catch return error.InvalidValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cval(self: BackgroundBlur) u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.false => 0,
|
||||||
|
.true => 20,
|
||||||
|
.radius => |v| v,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formatEntry(
|
||||||
|
self: BackgroundBlur,
|
||||||
|
formatter: anytype,
|
||||||
|
) !void {
|
||||||
|
switch (self) {
|
||||||
|
.false => try formatter.formatEntry(bool, false),
|
||||||
|
.true => try formatter.formatEntry(bool, true),
|
||||||
|
.radius => |v| try formatter.formatEntry(u8, v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse BackgroundBlur" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var v: BackgroundBlur = undefined;
|
||||||
|
|
||||||
|
try v.parseCLI(null);
|
||||||
|
try testing.expectEqual(.true, v);
|
||||||
|
|
||||||
|
try v.parseCLI("true");
|
||||||
|
try testing.expectEqual(.true, v);
|
||||||
|
|
||||||
|
try v.parseCLI("false");
|
||||||
|
try testing.expectEqual(.false, v);
|
||||||
|
|
||||||
|
try v.parseCLI("42");
|
||||||
|
try testing.expectEqual(42, v.radius);
|
||||||
|
|
||||||
|
try testing.expectError(error.InvalidValue, v.parseCLI(""));
|
||||||
|
try testing.expectError(error.InvalidValue, v.parseCLI("aaaa"));
|
||||||
|
try testing.expectError(error.InvalidValue, v.parseCLI("420"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// See theme
|
/// See theme
|
||||||
pub const Theme = struct {
|
pub const Theme = struct {
|
||||||
light: []const u8,
|
light: []const u8,
|
||||||
|
@ -84,6 +84,17 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
|
|||||||
ptr.* = @intCast(@as(Backing, @bitCast(value)));
|
ptr.* = @intCast(@as(Backing, @bitCast(value)));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.Union => |_| {
|
||||||
|
if (@hasDecl(T, "cval")) {
|
||||||
|
const PtrT = @typeInfo(@TypeOf(T.cval)).Fn.return_type.?;
|
||||||
|
const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw));
|
||||||
|
ptr.* = value.cval();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
else => return false,
|
else => return false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -172,3 +183,30 @@ test "c_get: optional" {
|
|||||||
try testing.expectEqual(0, cval.b);
|
try testing.expectEqual(0, cval.b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "c_get: background-blur" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c = try Config.default(alloc);
|
||||||
|
defer c.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
c.@"background-blur-radius" = .false;
|
||||||
|
var cval: u8 = undefined;
|
||||||
|
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||||
|
try testing.expectEqual(0, cval);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
c.@"background-blur-radius" = .true;
|
||||||
|
var cval: u8 = undefined;
|
||||||
|
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||||
|
try testing.expectEqual(20, cval);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
c.@"background-blur-radius" = .{ .radius = 42 };
|
||||||
|
var cval: u8 = undefined;
|
||||||
|
try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
|
||||||
|
try testing.expectEqual(42, cval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user