mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-03 06:09:04 +00:00
Merge remote-tracking branch 'origin/main' into close-split-gtk
# Conflicts: # po/hu.po # po/id.po
This commit is contained in:
@@ -201,6 +201,9 @@ pub const Action = union(Key) {
|
||||
/// Set the title of the target to the requested value.
|
||||
set_title: SetTitle,
|
||||
|
||||
/// Set the tab title override for the target's tab.
|
||||
set_tab_title: SetTitle,
|
||||
|
||||
/// Set the title of the target to a prompted value. It is up to
|
||||
/// the apprt to prompt. The value specifies whether to prompt for the
|
||||
/// surface title or the tab title.
|
||||
@@ -375,6 +378,7 @@ pub const Action = union(Key) {
|
||||
render_inspector,
|
||||
desktop_notification,
|
||||
set_title,
|
||||
set_tab_title,
|
||||
prompt_title,
|
||||
pwd,
|
||||
mouse_shape,
|
||||
|
||||
@@ -50,10 +50,11 @@ pub const App = struct {
|
||||
/// Callback called to handle an action.
|
||||
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool,
|
||||
|
||||
/// Read the clipboard value. The return value must be preserved
|
||||
/// by the host until the next call. If there is no valid clipboard
|
||||
/// value then this should return null.
|
||||
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) void,
|
||||
/// Read the clipboard value. Returns true if the clipboard request
|
||||
/// was started and complete_clipboard_request may be called with the
|
||||
/// given state pointer. Returns false if the clipboard request couldn't
|
||||
/// be started (such as when no text is available for a paste request).
|
||||
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) bool,
|
||||
|
||||
/// This may be called after a read clipboard call to request
|
||||
/// confirmation that the clipboard value is safe to read. The embedder
|
||||
@@ -512,7 +513,15 @@ pub const Surface = struct {
|
||||
break :wd;
|
||||
}
|
||||
|
||||
config.@"working-directory" = wd;
|
||||
var wd_val: configpkg.WorkingDirectory = .{ .path = wd };
|
||||
if (wd_val.finalize(config.arenaAlloc())) |_| {
|
||||
config.@"working-directory" = wd_val;
|
||||
} else |err| {
|
||||
log.warn(
|
||||
"error finalizing working directory config dir={s} err={}",
|
||||
.{ wd_val.path, err },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,14 +681,16 @@ pub const Surface = struct {
|
||||
errdefer alloc.destroy(state_ptr);
|
||||
state_ptr.* = state;
|
||||
|
||||
self.app.opts.read_clipboard(
|
||||
const started = self.app.opts.read_clipboard(
|
||||
self.userdata,
|
||||
@intCast(@intFromEnum(clipboard_type)),
|
||||
state_ptr,
|
||||
);
|
||||
if (!started) {
|
||||
alloc.destroy(state_ptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Embedded apprt can't synchronously check clipboard content types,
|
||||
// so we always return true to indicate the request was started.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
_ = @import("gtk/ext.zig");
|
||||
_ = @import("gtk/key.zig");
|
||||
_ = @import("gtk/portal.zig");
|
||||
}
|
||||
|
||||
@@ -22,16 +22,10 @@ const log = std.log.scoped(.gtk);
|
||||
pub const must_draw_from_app_thread = true;
|
||||
|
||||
/// GTK application ID
|
||||
pub const application_id = switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => "com.mitchellh.ghostty-debug",
|
||||
.ReleaseFast, .ReleaseSmall => "com.mitchellh.ghostty",
|
||||
};
|
||||
pub const application_id = @import("build/info.zig").application_id;
|
||||
|
||||
/// GTK object path
|
||||
pub const object_path = switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => "/com/mitchellh/ghostty_debug",
|
||||
.ReleaseFast, .ReleaseSmall => "/com/mitchellh/ghostty",
|
||||
};
|
||||
pub const object_path = @import("build/info.zig").object_path;
|
||||
|
||||
/// The GObject Application instance
|
||||
app: *Application,
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Prefix/appid for the gresource file.
|
||||
pub const prefix = "/com/mitchellh/ghostty";
|
||||
pub const app_id = "com.mitchellh.ghostty";
|
||||
const build_info = @import("info.zig");
|
||||
|
||||
/// The path to the Blueprint files. The folder structure is expected to be
|
||||
/// `{version}/{name}.blp` where `version` is the major and minor
|
||||
@@ -112,7 +110,7 @@ pub fn blueprint(comptime bp: Blueprint) [:0]const u8 {
|
||||
std.mem.eql(u8, candidate.name, bp.name))
|
||||
{
|
||||
return std.fmt.comptimePrint("{s}/ui/{d}.{d}/{s}.ui", .{
|
||||
prefix,
|
||||
build_info.resource_path,
|
||||
candidate.major,
|
||||
candidate.minor,
|
||||
candidate.name,
|
||||
@@ -173,7 +171,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
|
||||
try writer.print(
|
||||
\\ <gresource prefix="{s}/icons">
|
||||
\\
|
||||
, .{prefix});
|
||||
, .{build_info.resource_path});
|
||||
|
||||
const cwd = std.fs.cwd();
|
||||
inline for (icon_sizes) |size| {
|
||||
@@ -186,7 +184,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
|
||||
\\ <file alias="{s}/apps/{s}.png">{s}</file>
|
||||
\\
|
||||
,
|
||||
.{ alias, app_id, source },
|
||||
.{ alias, build_info.base_application_id, source },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,7 +197,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
|
||||
\\ <file alias="{s}/apps/{s}.png">{s}</file>
|
||||
\\
|
||||
,
|
||||
.{ alias, app_id, source },
|
||||
.{ alias, build_info.base_application_id, source },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -215,7 +213,7 @@ fn genRoot(writer: *std.Io.Writer) !void {
|
||||
try writer.print(
|
||||
\\ <gresource prefix="{s}">
|
||||
\\
|
||||
, .{prefix});
|
||||
, .{build_info.resource_path});
|
||||
|
||||
const cwd = std.fs.cwd();
|
||||
inline for (css) |name| {
|
||||
@@ -249,7 +247,7 @@ fn genUi(
|
||||
try writer.print(
|
||||
\\ <gresource prefix="{s}/ui">
|
||||
\\
|
||||
, .{prefix});
|
||||
, .{build_info.resource_path});
|
||||
|
||||
for (files.items) |ui_file| {
|
||||
for (blueprints) |bp| {
|
||||
|
||||
18
src/apprt/gtk/build/info.zig
Normal file
18
src/apprt/gtk/build/info.zig
Normal file
@@ -0,0 +1,18 @@
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Base application ID
|
||||
pub const base_application_id = "com.mitchellh.ghostty";
|
||||
|
||||
/// GTK application ID
|
||||
pub const application_id = switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => base_application_id ++ "-debug",
|
||||
.ReleaseFast, .ReleaseSmall => base_application_id,
|
||||
};
|
||||
|
||||
pub const resource_path = "/com/mitchellh/ghostty";
|
||||
|
||||
/// GTK object path
|
||||
pub const object_path = switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => resource_path ++ "_debug",
|
||||
.ReleaseFast, .ReleaseSmall => resource_path,
|
||||
};
|
||||
@@ -9,6 +9,7 @@ const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const build_info = @import("../build/info.zig");
|
||||
const state = &@import("../../../global.zig").state;
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
@@ -40,6 +41,7 @@ const Tab = @import("tab.zig").Tab;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
|
||||
const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
|
||||
const OpenURI = @import("../portal.zig").OpenURI;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_application);
|
||||
|
||||
@@ -214,6 +216,8 @@ pub const Application = extern struct {
|
||||
/// not exist in Ghostty's environment variable.
|
||||
saved_language: ?[:0]const u8 = null,
|
||||
|
||||
open_uri: OpenURI = undefined,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
@@ -327,7 +331,7 @@ pub const Application = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
break :app_id ApprtApp.application_id;
|
||||
break :app_id build_info.application_id;
|
||||
};
|
||||
|
||||
const display: *gdk.Display = gdk.Display.getDefault() orelse {
|
||||
@@ -350,7 +354,7 @@ pub const Application = extern struct {
|
||||
log.warn("error initializing windowing protocol err={}", .{err});
|
||||
break :wp .{ .none = .{} };
|
||||
};
|
||||
errdefer wp.deinit(alloc);
|
||||
errdefer wp.deinit();
|
||||
log.debug("windowing protocol={s}", .{@tagName(wp)});
|
||||
|
||||
// Create our GTK Application which encapsulates our process.
|
||||
@@ -381,7 +385,7 @@ pub const Application = extern struct {
|
||||
// Force the resource path to a known value so it doesn't depend
|
||||
// on the app id (which changes between debug/release and can be
|
||||
// user-configured) and force it to load in compiled resources.
|
||||
.resource_base_path = "/com/mitchellh/ghostty",
|
||||
.resource_base_path = build_info.resource_path,
|
||||
});
|
||||
|
||||
// Setup our private state. More setup is done in the init
|
||||
@@ -397,6 +401,7 @@ pub const Application = extern struct {
|
||||
.custom_css_providers = .empty,
|
||||
.global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}),
|
||||
.saved_language = saved_language,
|
||||
.open_uri = .init(rt_app),
|
||||
};
|
||||
|
||||
// Signals
|
||||
@@ -431,7 +436,8 @@ pub const Application = extern struct {
|
||||
const alloc = self.allocator();
|
||||
const priv: *Private = self.private();
|
||||
priv.config.unref();
|
||||
priv.winproto.deinit(alloc);
|
||||
priv.winproto.deinit();
|
||||
priv.open_uri.deinit();
|
||||
priv.global_shortcuts.unref();
|
||||
if (priv.saved_language) |language| alloc.free(language);
|
||||
if (gdk.Display.getDefault()) |display| {
|
||||
@@ -740,6 +746,7 @@ pub const Application = extern struct {
|
||||
.scrollbar => Action.scrollbar(target, value),
|
||||
|
||||
.set_title => Action.setTitle(target, value),
|
||||
.set_tab_title => return Action.setTabTitle(target, value),
|
||||
|
||||
.show_child_exited => return Action.showChildExited(target, value),
|
||||
|
||||
@@ -804,6 +811,11 @@ pub const Application = extern struct {
|
||||
return &self.private().winproto;
|
||||
}
|
||||
|
||||
/// Returns the open URI portal implementation.
|
||||
pub fn openUri(self: *Self) *OpenURI {
|
||||
return &self.private().open_uri;
|
||||
}
|
||||
|
||||
/// This will get called when there are no more open surfaces.
|
||||
fn startQuitTimer(self: *Self) void {
|
||||
const priv = self.private();
|
||||
@@ -1287,6 +1299,11 @@ pub const Application = extern struct {
|
||||
// Set ourselves as the default application.
|
||||
gio.Application.setDefault(self.as(gio.Application));
|
||||
|
||||
// The D-Bus connection is only valid after GApplication startup.
|
||||
self.openUri().setDbusConnection(
|
||||
self.as(gio.Application).getDbusConnection(),
|
||||
);
|
||||
|
||||
// Setup our event loop
|
||||
self.startupXev();
|
||||
|
||||
@@ -1871,6 +1888,17 @@ pub const Application = extern struct {
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn openUrlFallback(self: *Application, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
|
||||
// Fallback to the minimal cross-platform way of opening a URL.
|
||||
// This is always a safe fallback and enables for example Windows
|
||||
// to open URLs (GTK on Windows via WSL is a thing).
|
||||
internal_os.open(
|
||||
self.allocator(),
|
||||
kind,
|
||||
url,
|
||||
) catch |err| log.warn("unable to open url: {}", .{err});
|
||||
}
|
||||
};
|
||||
|
||||
/// All apprt action handlers
|
||||
@@ -2315,16 +2343,20 @@ const Action = struct {
|
||||
self: *Application,
|
||||
value: apprt.action.OpenUrl,
|
||||
) void {
|
||||
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
|
||||
if (std.mem.startsWith(u8, value.url, "/")) {
|
||||
self.openUrlFallback(value.kind, value.url);
|
||||
return;
|
||||
}
|
||||
if (std.mem.startsWith(u8, value.url, "file://")) {
|
||||
self.openUrlFallback(value.kind, value.url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to the minimal cross-platform way of opening a URL.
|
||||
// This is always a safe fallback and enables for example Windows
|
||||
// to open URLs (GTK on Windows via WSL is a thing).
|
||||
internal_os.open(
|
||||
self.allocator(),
|
||||
value.kind,
|
||||
value.url,
|
||||
) catch |err| log.warn("unable to open url: {}", .{err});
|
||||
self.openUri().start(value) catch |err| {
|
||||
log.err("unable to open uri err={}", .{err});
|
||||
self.openUrlFallback(value.kind, value.url);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pwd(
|
||||
@@ -2545,6 +2577,30 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTabTitle(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.SetTitle,
|
||||
) bool {
|
||||
switch (target) {
|
||||
.app => {
|
||||
log.warn("set_tab_title to app is unexpected", .{});
|
||||
return false;
|
||||
},
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
const tab = ext.getAncestor(
|
||||
Tab,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a tab, ignoring set_tab_title", .{});
|
||||
return false;
|
||||
};
|
||||
tab.setTitleOverride(if (value.title.len == 0) null else value.title);
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn showChildExited(
|
||||
target: apprt.Target,
|
||||
value: apprt.surface.Message.ChildExited,
|
||||
|
||||
@@ -353,7 +353,7 @@ pub const CommandPalette = extern struct {
|
||||
// Regular command - emit trigger signal
|
||||
const action = cmd.getAction() orelse return;
|
||||
|
||||
// Signal that an an action has been selected. Signals are synchronous
|
||||
// Signal that an action has been selected. Signals are synchronous
|
||||
// so we shouldn't need to worry about cloning the action.
|
||||
signals.trigger.impl.emit(
|
||||
self,
|
||||
|
||||
@@ -35,6 +35,7 @@ const TitleDialog = @import("title_dialog.zig").TitleDialog;
|
||||
const Window = @import("window.zig").Window;
|
||||
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
||||
const i18n = @import("../../../os/i18n.zig");
|
||||
const media = @import("../media.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_surface);
|
||||
|
||||
@@ -823,10 +824,11 @@ pub const Surface = extern struct {
|
||||
/// should be applied to the surface
|
||||
fn closureShouldUnfocusedSplitBeShown(
|
||||
_: *Self,
|
||||
search_active: c_int,
|
||||
focused: c_int,
|
||||
is_split: c_int,
|
||||
) callconv(.c) c_int {
|
||||
return @intFromBool(focused == 0 and is_split != 0);
|
||||
return @intFromBool(search_active == 0 and focused == 0 and is_split != 0);
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Self) void {
|
||||
@@ -1012,6 +1014,14 @@ pub const Surface = extern struct {
|
||||
priv.progress_bar_timer = null;
|
||||
}
|
||||
|
||||
if (priv.config) |config| {
|
||||
if (!config.get().@"progress-style") {
|
||||
log.debug("progress_report action blocked by config", .{});
|
||||
priv.progress_bar_overlay.as(gtk.Widget).setVisible(@intFromBool(false));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const progress_bar = priv.progress_bar_overlay;
|
||||
switch (value.state) {
|
||||
// Remove the progress bar
|
||||
@@ -2448,34 +2458,8 @@ pub const Surface = extern struct {
|
||||
1.0,
|
||||
);
|
||||
|
||||
assert(std.fs.path.isAbsolute(path));
|
||||
const media_file = gtk.MediaFile.newForFilename(path);
|
||||
|
||||
// If the audio file is marked as required, we'll emit an error if
|
||||
// there was a problem playing it. Otherwise there will be silence.
|
||||
if (required) {
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
mediaFileError,
|
||||
null,
|
||||
.{ .detail = "error" },
|
||||
);
|
||||
}
|
||||
|
||||
// Watch for the "ended" signal so that we can clean up after
|
||||
// ourselves.
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
mediaFileEnded,
|
||||
null,
|
||||
.{ .detail = "ended" },
|
||||
);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
media_stream.setVolume(volume);
|
||||
media_stream.play();
|
||||
const media_file = media.fromFilename(path) orelse break :audio;
|
||||
media.playMediaFile(media_file, volume, required);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3380,12 +3364,20 @@ pub const Surface = extern struct {
|
||||
config.command = try c.clone(config._arena.?.allocator());
|
||||
}
|
||||
if (priv.overrides.working_directory) |wd| {
|
||||
config.@"working-directory" = try config._arena.?.allocator().dupeZ(u8, wd);
|
||||
const config_alloc = config.arenaAlloc();
|
||||
var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, wd) };
|
||||
try wd_val.finalize(config_alloc);
|
||||
config.@"working-directory" = wd_val;
|
||||
}
|
||||
|
||||
// Properties that can impact surface init
|
||||
if (priv.font_size_request) |size| config.@"font-size" = size.points;
|
||||
if (priv.pwd) |pwd| config.@"working-directory" = pwd;
|
||||
if (priv.pwd) |pwd| {
|
||||
const config_alloc = config.arenaAlloc();
|
||||
var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, pwd) };
|
||||
try wd_val.finalize(config_alloc);
|
||||
config.@"working-directory" = wd_val;
|
||||
}
|
||||
|
||||
// Initialize the surface
|
||||
surface.init(
|
||||
@@ -3464,35 +3456,6 @@ pub const Surface = extern struct {
|
||||
right.setVisible(0);
|
||||
}
|
||||
|
||||
fn mediaFileError(
|
||||
media_file: *gtk.MediaFile,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const path = path: {
|
||||
const file = media_file.getFile() orelse break :path null;
|
||||
break :path file.getPath();
|
||||
};
|
||||
defer if (path) |p| glib.free(p);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
const err = media_stream.getError() orelse return;
|
||||
log.warn("error playing bell from {s}: {s} {d} {s}", .{
|
||||
path orelse "<<unknown>>",
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "",
|
||||
});
|
||||
}
|
||||
|
||||
fn mediaFileEnded(
|
||||
media_file: *gtk.MediaFile,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
media_file.unref();
|
||||
}
|
||||
|
||||
fn titleDialogSet(
|
||||
_: *TitleDialog,
|
||||
title_ptr: [*:0]const u8,
|
||||
|
||||
@@ -1071,21 +1071,6 @@ pub const Window = extern struct {
|
||||
self.syncAppearance();
|
||||
}
|
||||
|
||||
fn propGdkSurfaceHeight(
|
||||
_: *gdk.Surface,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// X11 needs to fix blurring on resize, but winproto implementations
|
||||
// could do anything.
|
||||
self.private().winproto.resizeEvent() catch |err| {
|
||||
log.warn(
|
||||
"winproto resize event failed error={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
fn propIsActive(
|
||||
_: *gtk.Window,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1111,7 +1096,7 @@ pub const Window = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn propGdkSurfaceWidth(
|
||||
fn propGdkSurfaceDims(
|
||||
_: *gdk.Surface,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
@@ -1250,7 +1235,7 @@ pub const Window = extern struct {
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
priv.tab_bindings.unref();
|
||||
priv.winproto.deinit(Application.default().allocator());
|
||||
priv.winproto.deinit();
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
@@ -1282,14 +1267,14 @@ pub const Window = extern struct {
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
gdk_surface,
|
||||
*Self,
|
||||
propGdkSurfaceWidth,
|
||||
propGdkSurfaceDims,
|
||||
self,
|
||||
.{ .detail = "width" },
|
||||
);
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
gdk_surface,
|
||||
*Self,
|
||||
propGdkSurfaceHeight,
|
||||
propGdkSurfaceDims,
|
||||
self,
|
||||
.{ .detail = "height" },
|
||||
);
|
||||
|
||||
102
src/apprt/gtk/media.zig
Normal file
102
src/apprt/gtk/media.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
const std = @import("std");
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
|
||||
const log = std.log.scoped(.gtk_media);
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
pub fn fromFilename(path: [:0]const u8) ?*gtk.MediaFile {
|
||||
assert(std.fs.path.isAbsolute(path));
|
||||
std.fs.accessAbsolute(path, .{ .mode = .read_only }) catch |err| {
|
||||
log.warn("unable to access {s}: {t}", .{ path, err });
|
||||
return null;
|
||||
};
|
||||
return gtk.MediaFile.newForFilename(path);
|
||||
}
|
||||
|
||||
pub fn fromResource(path: [:0]const u8) ?*gtk.MediaFile {
|
||||
assert(std.fs.path.isAbsolute(path));
|
||||
var gerr: ?*glib.Error = null;
|
||||
|
||||
const found = gio.resourcesGetInfo(path, .{}, null, null, &gerr);
|
||||
if (gerr) |err| {
|
||||
defer err.free();
|
||||
log.warn(
|
||||
"failed to find resource {s}: {s} {d} {s}",
|
||||
.{
|
||||
path,
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "(no message)",
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (found == 0) {
|
||||
log.warn("failed to find resource {s}", .{path});
|
||||
return null;
|
||||
}
|
||||
|
||||
return gtk.MediaFile.newForResource(path);
|
||||
}
|
||||
|
||||
pub fn playMediaFile(media_file: *gtk.MediaFile, volume: f64, required: bool) void {
|
||||
// If the audio file is marked as required, we'll emit an error if
|
||||
// there was a problem playing it. Otherwise there will be silence.
|
||||
if (required) {
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
mediaFileError,
|
||||
null,
|
||||
.{ .detail = "error" },
|
||||
);
|
||||
}
|
||||
|
||||
// Watch for the "ended" signal so that we can clean up after
|
||||
// ourselves.
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
mediaFileEnded,
|
||||
null,
|
||||
.{ .detail = "ended" },
|
||||
);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
media_stream.setVolume(volume);
|
||||
media_stream.play();
|
||||
}
|
||||
|
||||
fn mediaFileError(
|
||||
media_file: *gtk.MediaFile,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const path = path: {
|
||||
const file = media_file.getFile() orelse break :path null;
|
||||
break :path file.getPath();
|
||||
};
|
||||
defer if (path) |p| glib.free(p);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
const err = media_stream.getError() orelse return;
|
||||
log.warn("error playing sound from {s}: {s} {d} {s}", .{
|
||||
path orelse "<<unknown>>",
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "",
|
||||
});
|
||||
}
|
||||
|
||||
fn mediaFileEnded(
|
||||
media_file: *gtk.MediaFile,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
media_file.unref();
|
||||
}
|
||||
105
src/apprt/gtk/portal.zig
Normal file
105
src/apprt/gtk/portal.zig
Normal file
@@ -0,0 +1,105 @@
|
||||
const std = @import("std");
|
||||
|
||||
const gio = @import("gio");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const OpenURI = @import("portal/OpenURI.zig");
|
||||
pub const token_hex_len = @sizeOf(usize) * 2;
|
||||
pub const TokenBuffer = [token_hex_len + 1]u8;
|
||||
const token_format = std.fmt.comptimePrint("{{x:0>{}}}", .{token_hex_len});
|
||||
|
||||
/// Generate a token suitable for use in requests to the XDG Desktop Portal
|
||||
pub fn generateToken() usize {
|
||||
return std.crypto.random.int(usize);
|
||||
}
|
||||
|
||||
/// Format a request token consistently for use in portal object paths and payloads.
|
||||
pub fn formatToken(buf: *TokenBuffer, token: usize) [:0]const u8 {
|
||||
return std.fmt.bufPrintZ(buf, token_format, .{token}) catch unreachable;
|
||||
}
|
||||
|
||||
/// Get the XDG portal request path for the current Ghostty instance.
|
||||
///
|
||||
/// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
|
||||
/// for the protocol of the Request interface.
|
||||
pub fn getRequestPath(alloc: Allocator, dbus: *gio.DBusConnection, token: usize) (Allocator.Error || error{NoDBusUniqueName})![:0]const u8 {
|
||||
// Get the unique name from D-Bus and strip the leading `:`
|
||||
const unique_name = std.mem.span(
|
||||
dbus.getUniqueName() orelse {
|
||||
return error.NoDBusUniqueName;
|
||||
},
|
||||
)[1..];
|
||||
|
||||
return buildRequestPath(alloc, unique_name, token);
|
||||
}
|
||||
|
||||
/// Build the XDG portal request path for given unique name and token.
|
||||
fn buildRequestPath(alloc: Allocator, unique_name: []const u8, token: usize) Allocator.Error![:0]const u8 {
|
||||
var token_buf: TokenBuffer = undefined;
|
||||
const token_string = formatToken(&token_buf, token);
|
||||
|
||||
const object_path = try std.mem.joinZ(
|
||||
alloc,
|
||||
"/",
|
||||
&.{
|
||||
"/org/freedesktop/portal/desktop/request",
|
||||
unique_name,
|
||||
token_string,
|
||||
},
|
||||
);
|
||||
|
||||
// Sanitize the unique name by replacing every `.` with `_`. In effect, this
|
||||
// will turn a unique name like `1.192` into `1_192`.
|
||||
// This sounds arbitrary, but it's part of the Request protocol.
|
||||
_ = std.mem.replaceScalar(u8, object_path, '.', '_');
|
||||
|
||||
return object_path;
|
||||
}
|
||||
|
||||
/// Try and parse the token out of a request path.
|
||||
pub fn parseRequestPathToken(request_path: []const u8) ?usize {
|
||||
const index = std.mem.lastIndexOfScalar(u8, request_path, '/') orelse return null;
|
||||
const token = request_path[index + 1 ..];
|
||||
return std.fmt.parseUnsigned(usize, token, 16) catch return null;
|
||||
}
|
||||
|
||||
test "formatToken pads to fixed width" {
|
||||
const testing = std.testing;
|
||||
|
||||
var token_buf: TokenBuffer = undefined;
|
||||
const token = formatToken(&token_buf, 0x42);
|
||||
|
||||
try testing.expectEqual(@as(usize, token_hex_len), token.len);
|
||||
try testing.expectEqualStrings("0000000000000042", token);
|
||||
}
|
||||
|
||||
test "buildRequestPath" {
|
||||
const testing = std.testing;
|
||||
|
||||
const path = try buildRequestPath(testing.allocator, "1.42", 0x75af01a79c6fea34);
|
||||
try testing.expectEqualStrings(
|
||||
"/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34",
|
||||
path,
|
||||
);
|
||||
testing.allocator.free(path);
|
||||
}
|
||||
|
||||
test "buildRequestPath pads token" {
|
||||
const testing = std.testing;
|
||||
const path = try buildRequestPath(testing.allocator, "1.42", 0x42);
|
||||
|
||||
try testing.expectEqualStrings(
|
||||
"/org/freedesktop/portal/desktop/request/1_42/0000000000000042",
|
||||
path,
|
||||
);
|
||||
testing.allocator.free(path);
|
||||
}
|
||||
|
||||
test "parseRequestPathToken" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(0x75af01a79c6fea34, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34").?);
|
||||
try testing.expectEqual(null, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fGa34"));
|
||||
try testing.expectEqual(null, parseRequestPathToken("75af01a79c6fea34"));
|
||||
}
|
||||
565
src/apprt/gtk/portal/OpenURI.zig
Normal file
565
src/apprt/gtk/portal/OpenURI.zig
Normal file
@@ -0,0 +1,565 @@
|
||||
//! Use DBus to call the XDG Desktop Portal to open an URI.
|
||||
//! See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html#org-freedesktop-portal-openuri-openuri
|
||||
const OpenURI = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("../App.zig");
|
||||
const portal = @import("../portal.zig");
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
|
||||
const log = std.log.scoped(.openuri);
|
||||
|
||||
/// The GTK app that we "belong" to.
|
||||
app: *App,
|
||||
|
||||
/// Connection to the D-Bus session bus that we'll use for all of our messaging.
|
||||
dbus: ?*gio.DBusConnection = null,
|
||||
|
||||
/// Mutex to protect modification of the entries map or the cleanup timer.
|
||||
mutex: std.Thread.Mutex = .{},
|
||||
|
||||
/// Map to store data about any in-flight calls to the portal.
|
||||
entries: std.AutoArrayHashMapUnmanaged(usize, *Entry) = .empty,
|
||||
|
||||
/// Used to manage a timer to clean up any orphan entries in the map.
|
||||
cleanup_timer: ?c_uint = null,
|
||||
|
||||
/// Set to false during shutdown so callbacks stop touching internal state.
|
||||
alive: bool = true,
|
||||
|
||||
const RequestData = struct {
|
||||
open_uri: *OpenURI,
|
||||
token: usize,
|
||||
kind: apprt.action.OpenUrl.Kind,
|
||||
uri: [:0]const u8,
|
||||
request_path: [:0]const u8,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
open_uri: *OpenURI,
|
||||
token: usize,
|
||||
kind: apprt.action.OpenUrl.Kind,
|
||||
uri: []const u8,
|
||||
request_path: []const u8,
|
||||
) Allocator.Error!*RequestData {
|
||||
const uri_copy = try alloc.dupeZ(u8, uri);
|
||||
errdefer alloc.free(uri_copy);
|
||||
|
||||
const request_path_copy = try alloc.dupeZ(u8, request_path);
|
||||
errdefer alloc.free(request_path_copy);
|
||||
|
||||
const data = try alloc.create(RequestData);
|
||||
errdefer alloc.destroy(data);
|
||||
|
||||
data.* = .{
|
||||
.open_uri = open_uri,
|
||||
.token = token,
|
||||
.kind = kind,
|
||||
.uri = uri_copy,
|
||||
.request_path = request_path_copy,
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const RequestData, alloc: Allocator) void {
|
||||
alloc.free(self.uri);
|
||||
alloc.free(self.request_path);
|
||||
}
|
||||
};
|
||||
|
||||
/// Data about any in-flight calls to the portal.
|
||||
pub const Entry = struct {
|
||||
/// When the request started.
|
||||
start: std.time.Instant,
|
||||
/// A token used by the portal to identify requests and responses. The
|
||||
/// actual format of the token does not really matter as long as it can be
|
||||
/// used as part of a D-Bus object path. `usize` was chosen since it's easy
|
||||
/// to hash and to generate random tokens.
|
||||
token: usize,
|
||||
/// The "kind" of URI. Unused here, but we may need to pass it on to the
|
||||
/// fallback URL opener if the D-Bus method fails.
|
||||
kind: apprt.action.OpenUrl.Kind,
|
||||
/// A copy of the URI that we are opening. We need our own copy since the
|
||||
/// method calls are asynchronous and the original may have been freed by
|
||||
/// the time we need it.
|
||||
uri: [:0]const u8,
|
||||
/// Used to manage a subscription to a D-Bus signal, which is how the XDG
|
||||
/// Portal reports results of the method call.
|
||||
subscription: ?c_uint = null,
|
||||
|
||||
pub fn deinit(self: *const Entry, alloc: Allocator) void {
|
||||
alloc.free(self.uri);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Errors = error{
|
||||
/// Could not get a D-Bus connection
|
||||
DBusConnectionRequired,
|
||||
/// The D-Bus connection did not have a unique name. This _should_ be
|
||||
/// impossible, but is handled for safety's sake.
|
||||
NoDBusUniqueName,
|
||||
/// The system was unable to give us the time.
|
||||
TimerUnavailable,
|
||||
};
|
||||
|
||||
pub fn init(app: *App) OpenURI {
|
||||
return .{
|
||||
.app = app,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setDbusConnection(self: *OpenURI, dbus: ?*gio.DBusConnection) void {
|
||||
self.dbus = dbus;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *OpenURI) void {
|
||||
const alloc = self.app.app.allocator();
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (!self.alive) return;
|
||||
self.alive = false;
|
||||
|
||||
self.stopCleanupTimer();
|
||||
|
||||
for (self.entries.entries.items(.value)) |entry| {
|
||||
self.unsubscribeFromResponse(entry);
|
||||
destroyEntry(alloc, entry);
|
||||
}
|
||||
|
||||
self.entries.deinit(alloc);
|
||||
self.entries = .empty;
|
||||
self.dbus = null;
|
||||
}
|
||||
|
||||
/// Send the D-Bus method call to the XDG Desktop portal. The result of the
|
||||
/// method call will be reported asynchronously.
|
||||
pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || Errors)!void {
|
||||
const alloc = self.app.app.allocator();
|
||||
const dbus = self.dbus orelse return error.DBusConnectionRequired;
|
||||
|
||||
const token = portal.generateToken();
|
||||
const request_path = try portal.getRequestPath(alloc, dbus, token);
|
||||
defer alloc.free(request_path);
|
||||
|
||||
const request = try RequestData.init(alloc, self, token, value.kind, value.url, request_path);
|
||||
errdefer {
|
||||
request.deinit(alloc);
|
||||
alloc.destroy(request);
|
||||
}
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
// Create an entry that is used to track the results of the D-Bus method
|
||||
// call.
|
||||
const entry = entry: {
|
||||
const entry = try alloc.create(Entry);
|
||||
errdefer alloc.destroy(entry);
|
||||
entry.* = .{
|
||||
.start = std.time.Instant.now() catch return error.TimerUnavailable,
|
||||
.token = token,
|
||||
.kind = value.kind,
|
||||
.uri = try alloc.dupeZ(u8, value.url),
|
||||
};
|
||||
errdefer entry.deinit(alloc);
|
||||
try self.entries.putNoClobber(alloc, token, entry);
|
||||
break :entry entry;
|
||||
};
|
||||
|
||||
errdefer {
|
||||
_ = self.entries.swapRemove(token);
|
||||
destroyEntry(alloc, entry);
|
||||
}
|
||||
|
||||
self.startCleanupTimer();
|
||||
self.subscribeToResponse(entry, dbus, request_path.ptr);
|
||||
self.sendRequest(entry, dbus, request);
|
||||
}
|
||||
|
||||
/// Subscribe to the D-Bus signal that will contain the results of our method
|
||||
/// call to the portal. This must be called with the mutex locked.
|
||||
fn subscribeToResponse(
|
||||
self: *OpenURI,
|
||||
entry: *Entry,
|
||||
dbus: *gio.DBusConnection,
|
||||
request_path: [*:0]const u8,
|
||||
) void {
|
||||
assert(!self.mutex.tryLock());
|
||||
|
||||
if (entry.subscription != null) return;
|
||||
|
||||
entry.subscription = dbus.signalSubscribe(
|
||||
null,
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
request_path,
|
||||
null,
|
||||
.{},
|
||||
responseReceived,
|
||||
self,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unsubscribe to the D-Bus signal that contains the result of the method call.
|
||||
/// This will prevent a response from being processed multiple times. This must
|
||||
/// be called when the mutex is locked.
|
||||
fn unsubscribeFromResponse(self: *OpenURI, entry: *Entry) void {
|
||||
assert(!self.mutex.tryLock());
|
||||
|
||||
// Unsubscribe from the response signal
|
||||
if (entry.subscription) |subscription| {
|
||||
const dbus = self.dbus orelse {
|
||||
entry.subscription = null;
|
||||
log.warn("unable to unsubscribe open uri response without dbus connection", .{});
|
||||
return;
|
||||
};
|
||||
dbus.signalUnsubscribe(subscription);
|
||||
entry.subscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyEntry(alloc: Allocator, entry: *Entry) void {
|
||||
entry.deinit(alloc);
|
||||
alloc.destroy(entry);
|
||||
}
|
||||
|
||||
fn failRequest(self: *OpenURI, token: usize) ?*Entry {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (!self.alive) return null;
|
||||
|
||||
const entry = (self.entries.fetchSwapRemove(token) orelse return null).value;
|
||||
self.unsubscribeFromResponse(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
fn failRequestAndFallback(self: *OpenURI, request: *const RequestData) void {
|
||||
const alloc = self.app.app.allocator();
|
||||
const entry = self.failRequest(request.token) orelse return;
|
||||
defer destroyEntry(alloc, entry);
|
||||
|
||||
self.app.app.openUrlFallback(request.kind, request.uri);
|
||||
}
|
||||
|
||||
/// Send the D-Bus method call to the portal. The mutex must be locked when this
|
||||
/// is called.
|
||||
fn sendRequest(
|
||||
self: *OpenURI,
|
||||
entry: *Entry,
|
||||
dbus: *gio.DBusConnection,
|
||||
request: *RequestData,
|
||||
) void {
|
||||
assert(!self.mutex.tryLock());
|
||||
|
||||
const payload = payload: {
|
||||
const builder_type = glib.VariantType.new("(ssa{sv})");
|
||||
defer builder_type.free();
|
||||
|
||||
// Initialize our builder to build up our parameters
|
||||
var builder: glib.VariantBuilder = undefined;
|
||||
builder.init(builder_type);
|
||||
|
||||
// parent window - empty string means we have no window
|
||||
builder.add("s", "");
|
||||
|
||||
// URI to open
|
||||
builder.add("s", entry.uri.ptr);
|
||||
|
||||
// Options
|
||||
{
|
||||
const options = glib.VariantType.new("a{sv}");
|
||||
defer options.free();
|
||||
|
||||
builder.open(options);
|
||||
defer builder.close();
|
||||
|
||||
{
|
||||
const option = glib.VariantType.new("{sv}");
|
||||
defer option.free();
|
||||
|
||||
builder.open(option);
|
||||
defer builder.close();
|
||||
|
||||
builder.add("s", "handle_token");
|
||||
|
||||
var token_buf: portal.TokenBuffer = undefined;
|
||||
const token = portal.formatToken(&token_buf, entry.token);
|
||||
|
||||
const handle_token = glib.Variant.newString(token.ptr);
|
||||
builder.add("v", handle_token);
|
||||
}
|
||||
{
|
||||
const option = glib.VariantType.new("{sv}");
|
||||
defer option.free();
|
||||
|
||||
builder.open(option);
|
||||
defer builder.close();
|
||||
|
||||
builder.add("s", "writable");
|
||||
|
||||
const writable = glib.Variant.newBoolean(@intFromBool(false));
|
||||
builder.add("v", writable);
|
||||
}
|
||||
{
|
||||
const option = glib.VariantType.new("{sv}");
|
||||
defer option.free();
|
||||
|
||||
builder.open(option);
|
||||
defer builder.close();
|
||||
|
||||
builder.add("s", "ask");
|
||||
|
||||
const ask = glib.Variant.newBoolean(@intFromBool(false));
|
||||
builder.add("v", ask);
|
||||
}
|
||||
}
|
||||
|
||||
break :payload builder.end();
|
||||
};
|
||||
|
||||
// We're expecting an object path back from the method call.
|
||||
const reply_type = glib.VariantType.new("(o)");
|
||||
defer reply_type.free();
|
||||
|
||||
dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.OpenURI",
|
||||
"OpenURI",
|
||||
payload,
|
||||
reply_type,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
requestCallback,
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
/// Process the result of the original method call. Receiving this result does
|
||||
/// not indicate that the that the method call succeeded but it may contain an
|
||||
/// error message that is useful to log for debugging purposes.
|
||||
fn requestCallback(
|
||||
source: ?*gobject.Object,
|
||||
result: *gio.AsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const request: *RequestData = @ptrCast(@alignCast(ud orelse return));
|
||||
const self = request.open_uri;
|
||||
const alloc = self.app.app.allocator();
|
||||
defer {
|
||||
request.deinit(alloc);
|
||||
alloc.destroy(request);
|
||||
}
|
||||
|
||||
const dbus = gobject.ext.cast(gio.DBusConnection, source orelse {
|
||||
log.err("Open URI request finished without a D-Bus source object", .{});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
}) orelse {
|
||||
log.err("Open URI request finished with an unexpected source object", .{});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
};
|
||||
|
||||
var err_: ?*glib.Error = null;
|
||||
defer if (err_) |err| err.free();
|
||||
|
||||
const reply_ = dbus.callFinish(result, &err_);
|
||||
|
||||
if (err_) |err| {
|
||||
log.err("Open URI request failed={s} ({})", .{
|
||||
err.f_message orelse "(unknown)",
|
||||
err.f_code,
|
||||
});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
}
|
||||
|
||||
const reply = reply_ orelse {
|
||||
log.err("D-Bus method call returned a null value!", .{});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
};
|
||||
defer reply.unref();
|
||||
|
||||
const reply_type = glib.VariantType.new("(o)");
|
||||
defer reply_type.free();
|
||||
|
||||
if (reply.isOfType(reply_type) == 0) {
|
||||
log.warn("Reply from D-Bus method call does not contain an object path!", .{});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
}
|
||||
|
||||
var object_path: [*:0]const u8 = undefined;
|
||||
reply.get("(&o)", &object_path);
|
||||
|
||||
const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse {
|
||||
log.warn("Unable to parse token from the object path {s}", .{object_path});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
};
|
||||
|
||||
if (token != request.token) {
|
||||
log.warn("Open URI request returned mismatched token expected={x} actual={x}", .{
|
||||
request.token,
|
||||
token,
|
||||
});
|
||||
self.failRequestAndFallback(request);
|
||||
return;
|
||||
}
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (!self.alive) return;
|
||||
|
||||
const entry = self.entries.get(token) orelse return;
|
||||
if (std.mem.eql(u8, request.request_path, std.mem.span(object_path))) return;
|
||||
|
||||
log.debug("updating open uri request path old={s} new={s}", .{
|
||||
request.request_path,
|
||||
object_path,
|
||||
});
|
||||
self.unsubscribeFromResponse(entry);
|
||||
self.subscribeToResponse(entry, dbus, object_path);
|
||||
}
|
||||
|
||||
/// Handle the response signal from the portal. This should contain the actual
|
||||
/// results of the method call (success or failure).
|
||||
fn responseReceived(
|
||||
_: *gio.DBusConnection,
|
||||
_: ?[*:0]const u8,
|
||||
object_path: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
params: *glib.Variant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
|
||||
log.err("OpenURI response received with null userdata", .{});
|
||||
return;
|
||||
}));
|
||||
|
||||
const alloc = self.app.app.allocator();
|
||||
|
||||
const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse {
|
||||
log.warn("invalid object path: {s}", .{std.mem.span(object_path)});
|
||||
return;
|
||||
};
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (!self.alive) return;
|
||||
|
||||
const entry = (self.entries.fetchSwapRemove(token) orelse {
|
||||
log.warn("no entry for token {x}", .{token});
|
||||
return;
|
||||
}).value;
|
||||
|
||||
defer destroyEntry(alloc, entry);
|
||||
|
||||
self.unsubscribeFromResponse(entry);
|
||||
|
||||
var response: u32 = 0;
|
||||
var results: ?*glib.Variant = null;
|
||||
defer if (results) |variant| variant.unref();
|
||||
params.get("(u@a{sv})", &response, &results);
|
||||
|
||||
switch (response) {
|
||||
0 => {
|
||||
log.debug("open uri successful", .{});
|
||||
},
|
||||
1 => {
|
||||
log.debug("open uri request was cancelled by the user", .{});
|
||||
},
|
||||
2 => {
|
||||
log.warn("open uri request ended unexpectedly", .{});
|
||||
self.app.app.openUrlFallback(entry.kind, entry.uri);
|
||||
},
|
||||
else => {
|
||||
log.err("unrecognized response code={}", .{response});
|
||||
self.app.app.openUrlFallback(entry.kind, entry.uri);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait this number of seconds and then clean up any orphaned entries.
|
||||
const cleanup_timeout = 30;
|
||||
|
||||
/// If there is an active cleanup timer, cancel it. This must be called with the
|
||||
/// mutex locked
|
||||
fn stopCleanupTimer(self: *OpenURI) void {
|
||||
assert(!self.mutex.tryLock());
|
||||
|
||||
if (self.cleanup_timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove cleanup timer source={d}", .{timer});
|
||||
}
|
||||
self.cleanup_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a timer to clean up any entries that have not received a timely
|
||||
/// response. If there is already a timer it will be stopped and replaced with a
|
||||
/// new one. This must be called with the mutex locked.
|
||||
fn startCleanupTimer(self: *OpenURI) void {
|
||||
assert(!self.mutex.tryLock());
|
||||
|
||||
self.stopCleanupTimer();
|
||||
self.cleanup_timer = glib.timeoutAddSeconds(cleanup_timeout + 1, cleanup, self);
|
||||
}
|
||||
|
||||
/// The cleanup timer is used to free up any entries that may have failed
|
||||
/// to get a response in a timely manner.
|
||||
fn cleanup(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
|
||||
log.warn("cleanup called with null userdata", .{});
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}));
|
||||
|
||||
const alloc = self.app.app.allocator();
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
self.cleanup_timer = null;
|
||||
if (!self.alive) return @intFromBool(glib.SOURCE_REMOVE);
|
||||
|
||||
const now = std.time.Instant.now() catch {
|
||||
// `now()` should never fail, but if it does, don't crash, just return.
|
||||
// This might cause a small memory leak in rare circumstances but it
|
||||
// should get cleaned up the next time a URL is clicked.
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
};
|
||||
|
||||
loop: while (true) {
|
||||
for (self.entries.entries.items(.value)) |entry| {
|
||||
if (now.since(entry.start) > cleanup_timeout * std.time.ns_per_s) {
|
||||
log.warn("open uri request timed out token={x}", .{entry.token});
|
||||
self.unsubscribeFromResponse(entry);
|
||||
_ = self.entries.swapRemove(entry.token);
|
||||
self.app.app.openUrlFallback(entry.kind, entry.uri);
|
||||
destroyEntry(alloc, entry);
|
||||
continue :loop;
|
||||
}
|
||||
}
|
||||
break :loop;
|
||||
}
|
||||
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
||||
@@ -203,7 +203,7 @@ Overlay terminal_page {
|
||||
// Apply unfocused-split-fill and unfocused-split-opacity to current surface
|
||||
// this is only applied when a tab has more than one surface
|
||||
Revealer {
|
||||
reveal-child: bind $should_unfocused_split_be_shown(template.focused, template.is-split) as <bool>;
|
||||
reveal-child: bind $should_unfocused_split_be_shown(search_overlay.active, template.focused, template.is-split) as <bool>;
|
||||
transition-duration: 0;
|
||||
// This is all necessary so that the Revealer itself doesn't override
|
||||
// any input events from the other overlays. Namely, if you don't have
|
||||
@@ -221,7 +221,7 @@ Overlay terminal_page {
|
||||
|
||||
DropTarget drop_target {
|
||||
drop => $drop();
|
||||
actions: copy;
|
||||
actions: copy | move;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ pub const App = union(Protocol) {
|
||||
return .{ .none = .{} };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
pub fn deinit(self: *App) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
inline else => |*v| v.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@ pub const Window = union(Protocol) {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window, alloc: Allocator) void {
|
||||
pub fn deinit(self: *Window) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
inline else => |*v| v.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
187
src/apprt/gtk/winproto/BlurRegion.zig
Normal file
187
src/apprt/gtk/winproto/BlurRegion.zig
Normal file
@@ -0,0 +1,187 @@
|
||||
const BlurRegion = @This();
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Window = @import("../winproto.zig").Window;
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
|
||||
slices: std.ArrayList(Slice),
|
||||
|
||||
/// A rectangular slice of the blur region.
|
||||
// Marked `extern` since we want to be able to use this in X11 directly.
|
||||
pub const Slice = extern struct {
|
||||
x: Pos,
|
||||
y: Pos,
|
||||
width: Pos,
|
||||
height: Pos,
|
||||
};
|
||||
|
||||
// X11 compatibility. Ideally this should just be an `i32` like Wayland,
|
||||
// but XLib sucks
|
||||
const Pos = c_long;
|
||||
|
||||
pub const empty: BlurRegion = .{
|
||||
.slices = .empty,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *BlurRegion, alloc: Allocator) void {
|
||||
self.slices.deinit(alloc);
|
||||
self.slices = .empty;
|
||||
}
|
||||
|
||||
// Calculate the blur regions for a window.
|
||||
//
|
||||
// Since we have rounded corners by default, we need to carve out the
|
||||
// pixels on each corner to avoid the "korners bug".
|
||||
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
|
||||
pub fn calcForWindow(
|
||||
alloc: Allocator,
|
||||
window: *ApprtWindow,
|
||||
csd: bool,
|
||||
to_device_coordinates: bool,
|
||||
) Allocator.Error!BlurRegion {
|
||||
const native = window.as(gtk.Native);
|
||||
const surface = native.getSurface() orelse return .empty;
|
||||
|
||||
var slices: std.ArrayList(Slice) = .empty;
|
||||
errdefer slices.deinit(alloc);
|
||||
|
||||
// Calculate the primary blur region
|
||||
// (the one that covers most of the screen).
|
||||
// It's easier to do this inside a vector since we have to scale
|
||||
// everything by the scale factor anyways.
|
||||
|
||||
// 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.
|
||||
const x: Pos, const y: Pos = off: {
|
||||
var x: f64 = 0;
|
||||
var y: f64 = 0;
|
||||
native.getSurfaceTransform(&x, &y);
|
||||
// Slightly inset the corners if we're using CSDs
|
||||
if (csd) {
|
||||
x += 1;
|
||||
y += 1;
|
||||
}
|
||||
break :off .{ @intFromFloat(x), @intFromFloat(y) };
|
||||
};
|
||||
|
||||
var width = @as(Pos, surface.getWidth());
|
||||
var height = @as(Pos, surface.getHeight());
|
||||
|
||||
// Trim off the offsets. Be careful not to get negative.
|
||||
width -= x * 2;
|
||||
height -= y * 2;
|
||||
if (width <= 0 or height <= 0) return .empty;
|
||||
|
||||
// Empirically determined.
|
||||
const are_corners_rounded = rounded: {
|
||||
// This cast should always succeed as all of our windows
|
||||
// should be toplevel. If this fails, something very strange
|
||||
// is going on.
|
||||
const toplevel = gobject.ext.cast(
|
||||
gdk.Toplevel,
|
||||
surface,
|
||||
) orelse break :rounded false;
|
||||
|
||||
const state = toplevel.getState();
|
||||
if (state.fullscreen or state.maximized or state.tiled)
|
||||
break :rounded false;
|
||||
|
||||
break :rounded csd;
|
||||
};
|
||||
|
||||
const new_slices = try approxRoundedRect(
|
||||
alloc,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
// See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html#window-radius
|
||||
if (are_corners_rounded) 15 else 0,
|
||||
);
|
||||
|
||||
if (to_device_coordinates) {
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const sf = surface.getScaleFactor();
|
||||
for (new_slices.items) |*s| {
|
||||
s.x *= sf;
|
||||
s.y *= sf;
|
||||
s.width *= sf;
|
||||
s.height *= sf;
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .slices = new_slices };
|
||||
}
|
||||
|
||||
/// Whether two sets of blur regions are equal.
|
||||
pub fn eql(self: BlurRegion, other: BlurRegion) bool {
|
||||
if (self.slices.items.len != other.slices.items.len) return false;
|
||||
for (self.slices.items, other.slices.items) |this, that| {
|
||||
if (!std.meta.eql(this, that)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Approximate a rounded rectangle with many smaller rectangles.
|
||||
fn approxRoundedRect(
|
||||
alloc: Allocator,
|
||||
x: Pos,
|
||||
y: Pos,
|
||||
width: Pos,
|
||||
height: Pos,
|
||||
radius: Pos,
|
||||
) Allocator.Error!std.ArrayList(Slice) {
|
||||
const r_f: f32 = @floatFromInt(radius);
|
||||
|
||||
var slices: std.ArrayList(Slice) = .empty;
|
||||
errdefer slices.deinit(alloc);
|
||||
|
||||
// Add the central rectangle
|
||||
try slices.append(alloc, .{
|
||||
.x = x,
|
||||
.y = y + radius,
|
||||
.width = width,
|
||||
.height = height - 2 * radius,
|
||||
});
|
||||
|
||||
// Add the corner rows. This is honestly quite cursed.
|
||||
var row: Pos = 0;
|
||||
while (row < radius) : (row += 1) {
|
||||
// y distance from this row to the center corner circle
|
||||
const dy = @as(f32, @floatFromInt(radius - row)) - 0.5;
|
||||
|
||||
// x distance - as given by the definition of a circle
|
||||
const dx = @sqrt(r_f * r_f - dy * dy);
|
||||
|
||||
// How much each row should be offset, rounded to an integer
|
||||
const row_x: Pos = @intFromFloat(r_f - @round(dx + 0.5));
|
||||
|
||||
// Remove the offset from both ends
|
||||
const row_w = width - 2 * row_x;
|
||||
|
||||
// Top slice
|
||||
try slices.append(alloc, .{
|
||||
.x = x + row_x,
|
||||
.y = y + row,
|
||||
.width = row_w,
|
||||
.height = 1,
|
||||
});
|
||||
|
||||
// Bottom slice
|
||||
try slices.append(alloc, .{
|
||||
.x = x + row_x,
|
||||
.y = y + height - 1 - row,
|
||||
.width = row_w,
|
||||
.height = 1,
|
||||
});
|
||||
}
|
||||
|
||||
return slices;
|
||||
}
|
||||
@@ -19,9 +19,8 @@ pub const App = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
pub fn deinit(self: *App) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
@@ -47,9 +46,8 @@ pub const Window = struct {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
pub fn deinit(self: *Window) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
|
||||
@@ -7,49 +7,26 @@ const gdk_wayland = @import("gdk_wayland");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const layer_shell = @import("gtk4-layer-shell");
|
||||
|
||||
const wayland = @import("wayland");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const ext = wayland.client.ext;
|
||||
const kde = wayland.client.kde;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const Globals = @import("wayland/Globals.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
const BlurRegion = @import("BlurRegion.zig");
|
||||
|
||||
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,
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
||||
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
||||
///
|
||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
||||
/// creates a quick terminal, and we need to ensure this fails
|
||||
/// gracefully if this situation occurs.
|
||||
///
|
||||
/// FIXME: This is a temporary workaround - we should remove this when
|
||||
/// all of our supported distros drop support for affected old
|
||||
/// gtk4-layer-shell versions.
|
||||
///
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool = false,
|
||||
};
|
||||
globals: *Globals,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
@@ -69,34 +46,17 @@ pub const App = struct {
|
||||
gdk_wayland_display.getWlDisplay() 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;
|
||||
|
||||
// Do another round-trip to get the default decoration mode
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
const globals: *Globals = try .init(alloc, display);
|
||||
errdefer globals.deinit();
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.context = context,
|
||||
.globals = globals,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
alloc.destroy(self.context);
|
||||
pub fn deinit(self: *App) void {
|
||||
self.globals.deinit();
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
@@ -108,118 +68,23 @@ pub const App = struct {
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
_ = self;
|
||||
if (!layer_shell.isSupported()) {
|
||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.context.xdg_wm_dialog_present and
|
||||
layer_shell.getLibraryVersion().order(.{
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 4,
|
||||
}) == .lt)
|
||||
{
|
||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
|
||||
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
layer_shell.initForWindow(window);
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) return null;
|
||||
return T;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
||||
|
||||
switch (event) {
|
||||
.global => |v| {
|
||||
log.debug("found global {s}", .{v.interface});
|
||||
|
||||
// We don't actually do anything with this other than checking
|
||||
// for its existence, so we process this separately.
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
"xdg_wm_dialog_v1",
|
||||
) == .eq) {
|
||||
context.xdg_wm_dialog_present = true;
|
||||
return;
|
||||
}
|
||||
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
T.interface.name,
|
||||
) == .eq) {
|
||||
log.debug("matched {}", .{T});
|
||||
|
||||
@field(context, field.name) = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error binding interface {s} error={}",
|
||||
.{ v.interface, err },
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.default_mode => |mode| {
|
||||
context.default_deco_mode = @enumFromInt(mode.mode);
|
||||
},
|
||||
}
|
||||
// Set target monitor based on config (null lets compositor decide)
|
||||
const monitor = resolveQuickTerminalMonitor(self.globals, apprt_window);
|
||||
defer if (monitor) |v| v.unref();
|
||||
layer_shell.setMonitor(window, monitor);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,10 +96,10 @@ pub const Window = struct {
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// The context from the app where we can load our Wayland interfaces.
|
||||
app_context: *App.Context,
|
||||
globals: *Globals,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
/// Object that controls background effects like background blur.
|
||||
bg_effect: ?*ext.BackgroundEffectSurfaceV1 = null,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
@@ -248,6 +113,8 @@ pub const Window = struct {
|
||||
/// requesting attention from the user.
|
||||
activation_token: ?*xdg.ActivationTokenV1 = null,
|
||||
|
||||
blur_region: BlurRegion = .empty,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
@@ -272,7 +139,7 @@ pub const Window = struct {
|
||||
// Get our decoration object so we can control the
|
||||
// CSD vs SSD status of this surface.
|
||||
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
||||
const mgr = app.context.kde_decoration_manager orelse
|
||||
const mgr = app.globals.get(.kde_decoration_manager) orelse
|
||||
break :deco null;
|
||||
|
||||
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
||||
@@ -285,6 +152,20 @@ pub const Window = struct {
|
||||
break :deco deco;
|
||||
};
|
||||
|
||||
const bg_effect: ?*ext.BackgroundEffectSurfaceV1 = bg: {
|
||||
const mgr = app.globals.get(.ext_background_effect) orelse
|
||||
break :bg null;
|
||||
|
||||
const bg_effect: *ext.BackgroundEffectSurfaceV1 = mgr.getBackgroundEffect(
|
||||
wl_surface,
|
||||
) catch |err| {
|
||||
log.warn("could not create background effect object={}", .{err});
|
||||
break :bg null;
|
||||
};
|
||||
|
||||
break :bg bg_effect;
|
||||
};
|
||||
|
||||
if (apprt_window.isQuickTerminal()) {
|
||||
_ = gdk.Surface.signals.enter_monitor.connect(
|
||||
gdk_surface,
|
||||
@@ -298,26 +179,31 @@ pub const Window = struct {
|
||||
return .{
|
||||
.apprt_window = apprt_window,
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.globals = app.globals,
|
||||
.decoration = deco,
|
||||
.bg_effect = bg_effect,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.blur_region.deinit(self.globals.alloc);
|
||||
if (self.bg_effect) |bg| bg.destroy();
|
||||
if (self.decoration) |deco| deco.release();
|
||||
if (self.slide) |slide| slide.release();
|
||||
}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
self.syncDecoration() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
log.err("failed to sync decoration={}", .{err});
|
||||
};
|
||||
|
||||
if (self.apprt_window.isQuickTerminal()) {
|
||||
@@ -333,7 +219,7 @@ pub const Window = struct {
|
||||
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
|
||||
// However, if we do not support SSDs (e.g. GNOME) then we should enable
|
||||
// CSDs even if the user prefers SSDs.
|
||||
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
|
||||
.Server => if (self.globals.get(.kde_decoration_manager)) |_| false else true,
|
||||
.None => false,
|
||||
else => unreachable,
|
||||
};
|
||||
@@ -345,7 +231,7 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
const activation = self.globals.get(.xdg_activation) orelse return;
|
||||
|
||||
// If there already is a token, destroy and unset it
|
||||
if (self.activation_token) |token| token.destroy();
|
||||
@@ -361,28 +247,47 @@ pub const Window = struct {
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn syncBlur(self: *Window) !void {
|
||||
const manager = self.app_context.kde_blur_manager orelse return;
|
||||
const compositor = self.globals.get(.compositor) orelse return;
|
||||
const bg = self.bg_effect orelse return;
|
||||
if (!self.globals.state.bg_effect_capabilities.blur) return;
|
||||
|
||||
const config = if (self.apprt_window.getConfig()) |v|
|
||||
v.get()
|
||||
else
|
||||
return;
|
||||
const blur = config.@"background-blur";
|
||||
|
||||
if (self.blur_token) |tok| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blur.enabled()) {
|
||||
manager.unset(self.surface);
|
||||
tok.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blur.enabled()) {
|
||||
const tok = try manager.create(self.surface);
|
||||
tok.commit();
|
||||
self.blur_token = tok;
|
||||
}
|
||||
if (!blur.enabled()) {
|
||||
self.blur_region.deinit(self.globals.alloc);
|
||||
bg.setBlurRegion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var region: BlurRegion = try .calcForWindow(
|
||||
self.globals.alloc,
|
||||
self.apprt_window,
|
||||
self.clientSideDecorationEnabled(),
|
||||
false,
|
||||
);
|
||||
errdefer region.deinit(self.globals.alloc);
|
||||
|
||||
if (region.eql(self.blur_region)) {
|
||||
// Region didn't change. Don't do anything.
|
||||
region.deinit(self.globals.alloc);
|
||||
return;
|
||||
}
|
||||
|
||||
const wl_region = try compositor.createRegion();
|
||||
errdefer if (wl_region) |r| r.destroy();
|
||||
for (region.slices.items) |s| wl_region.add(
|
||||
@intCast(s.x),
|
||||
@intCast(s.y),
|
||||
@intCast(s.width),
|
||||
@intCast(s.height),
|
||||
);
|
||||
|
||||
bg.setBlurRegion(wl_region);
|
||||
self.blur_region = region;
|
||||
}
|
||||
|
||||
fn syncDecoration(self: *Window) !void {
|
||||
@@ -395,7 +300,7 @@ pub const Window = struct {
|
||||
|
||||
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
||||
return switch (self.apprt_window.getWindowDecoration()) {
|
||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
||||
.auto => self.globals.state.default_deco_mode orelse .Client,
|
||||
.client => .Client,
|
||||
.server => .Server,
|
||||
.none => .None,
|
||||
@@ -417,6 +322,12 @@ pub const Window = struct {
|
||||
});
|
||||
layer_shell.setNamespace(window, config.@"gtk-quick-terminal-namespace");
|
||||
|
||||
// Re-resolve the target monitor on every sync so that config reloads
|
||||
// and primary-output changes take effect without recreating the window.
|
||||
const target_monitor = resolveQuickTerminalMonitor(self.globals, self.apprt_window);
|
||||
defer if (target_monitor) |v| v.unref();
|
||||
layer_shell.setMonitor(window, target_monitor);
|
||||
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.@"quick-terminal-keyboard-interactivity") {
|
||||
@@ -457,7 +368,7 @@ pub const Window = struct {
|
||||
if (self.slide) |slide| slide.release();
|
||||
|
||||
self.slide = if (anchored_edge) |anchored| slide: {
|
||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
||||
const mgr = self.globals.get(.kde_slide_manager) orelse break :slide null;
|
||||
|
||||
const slide = mgr.create(self.surface) catch |err| {
|
||||
log.warn("could not create slide object={}", .{err});
|
||||
@@ -486,8 +397,17 @@ pub const Window = struct {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
const resolved_monitor = resolveQuickTerminalMonitor(
|
||||
apprt_window.winproto().wayland.globals,
|
||||
apprt_window,
|
||||
);
|
||||
defer if (resolved_monitor) |v| v.unref();
|
||||
|
||||
// Use the configured monitor for sizing if not in mouse mode.
|
||||
const size_monitor = resolved_monitor orelse monitor;
|
||||
|
||||
var monitor_size: gdk.Rectangle = undefined;
|
||||
monitor.getGeometry(&monitor_size);
|
||||
size_monitor.getGeometry(&monitor_size);
|
||||
|
||||
const dims = config.@"quick-terminal-size".calculate(
|
||||
config.@"quick-terminal-position",
|
||||
@@ -505,7 +425,7 @@ pub const Window = struct {
|
||||
event: xdg.ActivationTokenV1.Event,
|
||||
self: *Window,
|
||||
) void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
const activation = self.globals.get(.xdg_activation) orelse return;
|
||||
const current_token = self.activation_token orelse return;
|
||||
|
||||
if (token.getId() != current_token.getId()) {
|
||||
@@ -522,3 +442,45 @@ pub const Window = struct {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Resolve the quick-terminal-screen config to a specific monitor.
|
||||
/// Returns null to let the compositor decide (used for .mouse mode).
|
||||
/// Caller owns the returned ref and must unref it.
|
||||
fn resolveQuickTerminalMonitor(
|
||||
globals: *Globals,
|
||||
apprt_window: *ApprtWindow,
|
||||
) ?*gdk.Monitor {
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return null;
|
||||
|
||||
switch (config.@"quick-terminal-screen") {
|
||||
.mouse => return null,
|
||||
.main, .@"macos-menu-bar" => {},
|
||||
}
|
||||
|
||||
const display = apprt_window.as(gtk.Widget).getDisplay();
|
||||
const monitors = display.getMonitors();
|
||||
|
||||
// Try to find the monitor matching the primary output name.
|
||||
if (globals.state.primary_output_name) |stored_name| {
|
||||
var i: u32 = 0;
|
||||
while (monitors.getObject(i)) |item| : (i += 1) {
|
||||
const monitor = gobject.ext.cast(gdk.Monitor, item) orelse {
|
||||
item.unref();
|
||||
continue;
|
||||
};
|
||||
if (monitor.getConnector()) |connector_z| {
|
||||
if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) {
|
||||
return monitor;
|
||||
}
|
||||
}
|
||||
monitor.unref();
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the first monitor in the list.
|
||||
const first = monitors.getObject(0) orelse return null;
|
||||
return gobject.ext.cast(gdk.Monitor, first) orelse {
|
||||
first.unref();
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
252
src/apprt/gtk/winproto/wayland/Globals.zig
Normal file
252
src/apprt/gtk/winproto/wayland/Globals.zig
Normal file
@@ -0,0 +1,252 @@
|
||||
const Globals = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const ext = wayland.client.ext;
|
||||
const kde = wayland.client.kde;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
const log = std.log.scoped(.winproto_wayland_globals);
|
||||
|
||||
alloc: Allocator,
|
||||
state: State,
|
||||
map: std.EnumMap(Tag, Binding),
|
||||
|
||||
/// Used in the initial roundtrip to determine whether more
|
||||
/// roundtrips are required to fetch the initial state.
|
||||
needs_roundtrip: bool = false,
|
||||
|
||||
const Binding = struct {
|
||||
// All globals can be casted into a wl.Proxy object.
|
||||
proxy: *wl.Proxy,
|
||||
name: u32,
|
||||
};
|
||||
|
||||
pub const Tag = enum {
|
||||
compositor,
|
||||
ext_background_effect,
|
||||
kde_decoration_manager,
|
||||
kde_slide_manager,
|
||||
kde_output_order,
|
||||
xdg_activation,
|
||||
|
||||
fn Type(comptime self: Tag) type {
|
||||
return switch (self) {
|
||||
.compositor => wl.Compositor,
|
||||
.ext_background_effect => ext.BackgroundEffectManagerV1,
|
||||
.kde_decoration_manager => org.KdeKwinServerDecorationManager,
|
||||
.kde_slide_manager => org.KdeKwinSlideManager,
|
||||
.kde_output_order => kde.OutputOrderV1,
|
||||
.xdg_activation => xdg.ActivationV1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const State = struct {
|
||||
/// Connector name of the primary output (e.g., "DP-1") as reported
|
||||
/// by kde_output_order_v1. The first output in each priority list
|
||||
/// is the primary.
|
||||
primary_output_name: ?[:0]const u8 = null,
|
||||
|
||||
/// Tracks the output order event cycle. Set to true after a `done`
|
||||
/// event so the next `output` event is captured as the new primary.
|
||||
/// Initialized to true so the first event after binding is captured.
|
||||
output_order_done: bool = true,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
bg_effect_capabilities: ext.BackgroundEffectManagerV1.Capability = .{},
|
||||
|
||||
/// Reset cached state derived from kde_output_order_v1.
|
||||
fn resetOutputOrder(self: *State, alloc: Allocator) void {
|
||||
if (self.primary_output_name) |name| alloc.free(name);
|
||||
self.primary_output_name = null;
|
||||
self.output_order_done = true;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(alloc: Allocator, display: *wl.Display) !*Globals {
|
||||
// We need to allocate here since the listener
|
||||
// expects a stable memory address.
|
||||
const self = try alloc.create(Globals);
|
||||
self.* = .{
|
||||
.alloc = alloc,
|
||||
.state = .{},
|
||||
.map = .{},
|
||||
};
|
||||
|
||||
const registry = try display.getRegistry();
|
||||
registry.setListener(*Globals, registryListener, self);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
// Do another roundtrip to process events emitted by globals we bound
|
||||
// during registry discovery (e.g. default decoration mode, output
|
||||
// order). Listeners are installed at bind time in registryListener.
|
||||
if (self.needs_roundtrip) {
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Globals) void {
|
||||
if (self.state.primary_output_name) |name| self.alloc.free(name);
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn get(self: *const Globals, comptime tag: Tag) ?*tag.Type() {
|
||||
const binding = self.map.get(tag) orelse return null;
|
||||
return @ptrCast(binding.proxy);
|
||||
}
|
||||
|
||||
fn onGlobalAttached(self: *Globals, comptime tag: Tag) void {
|
||||
// Install listeners immediately at bind time. This
|
||||
// keeps listener setup and object lifetime in one
|
||||
// place and also supports globals that appear later.
|
||||
switch (tag) {
|
||||
.ext_background_effect => {
|
||||
const v = self.get(tag) orelse return;
|
||||
v.setListener(*Globals, bgEffectListener, self);
|
||||
self.needs_roundtrip = true;
|
||||
},
|
||||
.kde_decoration_manager => {
|
||||
const v = self.get(tag) orelse return;
|
||||
v.setListener(*Globals, decoManagerListener, self);
|
||||
self.needs_roundtrip = true;
|
||||
},
|
||||
.kde_output_order => {
|
||||
const v = self.get(tag) orelse return;
|
||||
v.setListener(*Globals, outputOrderListener, self);
|
||||
self.needs_roundtrip = true;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn onGlobalRemoved(self: *Globals, tag: Tag) void {
|
||||
switch (tag) {
|
||||
.kde_output_order => self.state.resetOutputOrder(self.alloc),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
self: *Globals,
|
||||
) void {
|
||||
switch (event) {
|
||||
.global => |v| {
|
||||
log.debug("found global {s}", .{v.interface});
|
||||
inline for (comptime std.meta.tags(Tag)) |tag| {
|
||||
const T = tag.Type();
|
||||
if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) {
|
||||
log.debug("matched {}", .{T});
|
||||
|
||||
const new_proxy = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error binding interface {s} error={}",
|
||||
.{ v.interface, err },
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
// If this global was already bound,
|
||||
// then we also need to destroy the old binding.
|
||||
if (self.map.get(tag)) |old| {
|
||||
self.onGlobalRemoved(tag);
|
||||
old.proxy.destroy();
|
||||
}
|
||||
|
||||
self.map.put(tag, .{
|
||||
.proxy = @ptrCast(new_proxy),
|
||||
.name = v.name,
|
||||
});
|
||||
self.onGlobalAttached(tag);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| {
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |kv| {
|
||||
if (kv.value.name != v.name) continue;
|
||||
self.onGlobalRemoved(kv.key);
|
||||
kv.value.proxy.destroy();
|
||||
self.map.remove(kv.key);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn bgEffectListener(
|
||||
_: *ext.BackgroundEffectManagerV1,
|
||||
event: ext.BackgroundEffectManagerV1.Event,
|
||||
self: *Globals,
|
||||
) void {
|
||||
switch (event) {
|
||||
.capabilities => |cap| {
|
||||
self.state.bg_effect_capabilities = cap.flags;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
self: *Globals,
|
||||
) void {
|
||||
switch (event) {
|
||||
.default_mode => |mode| {
|
||||
self.state.default_deco_mode = @enumFromInt(mode.mode);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn outputOrderListener(
|
||||
_: *kde.OutputOrderV1,
|
||||
event: kde.OutputOrderV1.Event,
|
||||
self: *Globals,
|
||||
) void {
|
||||
switch (event) {
|
||||
.output => |v| {
|
||||
// Only the first output event after a `done` is the new primary.
|
||||
if (!self.state.output_order_done) return;
|
||||
self.state.output_order_done = false;
|
||||
|
||||
const name = std.mem.sliceTo(v.output_name, 0);
|
||||
if (self.state.primary_output_name) |old| self.alloc.free(old);
|
||||
|
||||
if (name.len == 0) {
|
||||
self.state.primary_output_name = null;
|
||||
log.warn("ignoring empty primary output name from kde_output_order_v1", .{});
|
||||
} else {
|
||||
self.state.primary_output_name = self.alloc.dupeZ(u8, name) catch |err| {
|
||||
self.state.primary_output_name = null;
|
||||
log.warn("failed to allocate primary output name: {}", .{err});
|
||||
return;
|
||||
};
|
||||
log.debug("primary output: {s}", .{name});
|
||||
}
|
||||
},
|
||||
.done => {
|
||||
if (self.state.output_order_done) {
|
||||
// No output arrived since the previous done. Treat this as
|
||||
// an empty update and drop any stale cached primary.
|
||||
self.state.resetOutputOrder(self.alloc);
|
||||
return;
|
||||
}
|
||||
self.state.output_order_done = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub const c = @cImport({
|
||||
const input = @import("../../../input.zig");
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
const BlurRegion = @import("BlurRegion.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
@@ -48,7 +49,7 @@ pub const App = struct {
|
||||
else
|
||||
"ghostty";
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// Set the X11 window class property (WM_CLASS) if we are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
@@ -106,9 +107,8 @@ pub const App = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
pub fn deinit(self: *App) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event, and returns the
|
||||
@@ -170,13 +170,13 @@ pub const Window = struct {
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
x11_surface: *gdk_x11.X11Surface,
|
||||
alloc: Allocator,
|
||||
|
||||
blur_region: Region = .{},
|
||||
blur_region: BlurRegion = .empty,
|
||||
|
||||
// Cache last applied values to avoid redundant X11 property updates.
|
||||
// Redundant property updates seem to cause some visual glitches
|
||||
// with some window managers: https://github.com/ghostty-org/ghostty/pull/8075
|
||||
last_applied_blur_region: ?Region = null,
|
||||
last_applied_decoration_hints: ?MotifWMHints = null,
|
||||
|
||||
pub fn init(
|
||||
@@ -184,8 +184,6 @@ pub const Window = struct {
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const surface = apprt_window.as(gtk.Native).getSurface() orelse
|
||||
return error.NotX11Surface;
|
||||
|
||||
@@ -196,49 +194,31 @@ pub const Window = struct {
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.alloc = alloc,
|
||||
.apprt_window = apprt_window,
|
||||
.x11_surface = x11_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.blur_region.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
try self.syncBlur();
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
// The user could have toggled between CSDs and SSDs,
|
||||
// therefore we need to recalculate the blur region offset.
|
||||
self.blur_region = blur: {
|
||||
// NOTE(pluiedev): CSDs are a f--king mistake.
|
||||
// Please, GNOME, stop this nonsense of making a window ~30% bigger
|
||||
// internally than how they really are just for your shadows and
|
||||
// rounded corners and all that fluff. Please. I beg of you.
|
||||
var x: f64 = 0;
|
||||
var y: f64 = 0;
|
||||
|
||||
self.apprt_window.as(gtk.Native).getSurfaceTransform(&x, &y);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale: f64 = @floatFromInt(self.apprt_window.as(gtk.Widget).getScaleFactor());
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
};
|
||||
};
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to synchronize blur={}", .{err});
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
self.syncDecorations() catch |err| {
|
||||
log.err("failed to synchronize decorations={}", .{err});
|
||||
log.err("failed to sync decorations={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,53 +230,49 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
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 gtk_widget = self.apprt_window.as(gtk.Widget);
|
||||
const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
// When blur is disabled, remove the property if it was previously set
|
||||
const blur = config.@"background-blur";
|
||||
if (!blur.enabled()) {
|
||||
if (self.last_applied_blur_region != null) {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
self.last_applied_blur_region = null;
|
||||
}
|
||||
|
||||
var region: BlurRegion = if (blur.enabled())
|
||||
try .calcForWindow(
|
||||
self.alloc,
|
||||
self.apprt_window,
|
||||
self.clientSideDecorationEnabled(),
|
||||
true,
|
||||
)
|
||||
else
|
||||
.empty;
|
||||
errdefer region.deinit(self.alloc);
|
||||
|
||||
// Only update X11 properties when the blur region actually changes
|
||||
if (region.eql(self.blur_region)) {
|
||||
region.deinit(self.alloc);
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = gtk_widget.getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
if (region.slices.items.len > 0) {
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
region,
|
||||
});
|
||||
|
||||
// Only update X11 properties when the blur region actually changes
|
||||
if (self.last_applied_blur_region) |last| {
|
||||
if (std.meta.eql(self.blur_region, last)) return;
|
||||
try self.changeProperty(
|
||||
BlurRegion.Slice,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
region.slices.items,
|
||||
);
|
||||
} else {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
}
|
||||
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
self.last_applied_blur_region = self.blur_region;
|
||||
self.blur_region.deinit(self.alloc);
|
||||
self.blur_region = region;
|
||||
}
|
||||
|
||||
fn syncDecorations(self: *Window) !void {
|
||||
@@ -336,7 +312,7 @@ pub const Window = struct {
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&hints,
|
||||
&.{hints},
|
||||
);
|
||||
self.last_applied_decoration_hints = hints;
|
||||
}
|
||||
@@ -411,9 +387,11 @@ pub const Window = struct {
|
||||
options: struct {
|
||||
mode: PropertyChangeMode,
|
||||
},
|
||||
value: *T,
|
||||
values: []const T,
|
||||
) X11Error!void {
|
||||
const data: format.bufferType() = @ptrCast(value);
|
||||
const data: format.bufferType() = @ptrCast(@constCast(values));
|
||||
// The number of "words" that each element `T` occupies.
|
||||
const words_per_elem = @divExact(@sizeOf(T), @sizeOf(format.elemType()));
|
||||
|
||||
const status = c.XChangeProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
@@ -423,7 +401,7 @@ pub const Window = struct {
|
||||
@intFromEnum(format),
|
||||
@intFromEnum(options.mode),
|
||||
data,
|
||||
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
|
||||
@intCast(words_per_elem * values.len),
|
||||
);
|
||||
|
||||
// For some godforsaken reason Xlib alternates between
|
||||
@@ -499,13 +477,6 @@ const PropertyFormat = enum(c_int) {
|
||||
}
|
||||
};
|
||||
|
||||
const Region = extern struct {
|
||||
x: c_long = 0,
|
||||
y: c_long = 0,
|
||||
width: c_long = 0,
|
||||
height: c_long = 0,
|
||||
};
|
||||
|
||||
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
|
||||
const MotifWMHints = extern struct {
|
||||
flags: packed struct(c_ulong) {
|
||||
|
||||
@@ -188,7 +188,7 @@ pub fn newConfig(
|
||||
if (prev) |p| {
|
||||
if (shouldInheritWorkingDirectory(context, config)) {
|
||||
if (try p.pwd(alloc)) |pwd| {
|
||||
copy.@"working-directory" = pwd;
|
||||
copy.@"working-directory" = .{ .path = pwd };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user