gtk: implement quick terminal slide & autohide (#6090)

This commit is contained in:
Leah Amelia Chen
2025-03-05 23:20:03 +01:00
committed by GitHub
8 changed files with 98 additions and 25 deletions

View File

@ -81,6 +81,7 @@ pub const DerivedConfig = struct {
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
quick_terminal_position: configpkg.Config.QuickTerminalPosition,
quick_terminal_autohide: bool,
maximize: bool,
fullscreen: bool,
@ -98,6 +99,7 @@ pub const DerivedConfig = struct {
.gtk_toolbar_style = config.@"gtk-toolbar-style",
.quick_terminal_position = config.@"quick-terminal-position",
.quick_terminal_autohide = config.@"quick-terminal-autohide",
.maximize = config.maximize,
.fullscreen = config.fullscreen,
@ -247,6 +249,7 @@ pub fn init(self: *Window, app: *App) !void {
_ = c.g_signal_connect_data(self.window, "notify::maximized", c.G_CALLBACK(&gtkWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::fullscreened", c.G_CALLBACK(&gtkWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::is-active", c.G_CALLBACK(&gtkWindowNotifyIsActive), self, null, c.G_CONNECT_DEFAULT);
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
@ -378,9 +381,14 @@ pub fn present(self: *Window) void {
pub fn toggleVisibility(self: *Window) void {
const window: *gtk.Widget = @ptrCast(self.window);
window.setVisible(@intFromBool(window.isVisible() == 0));
}
pub fn isQuickTerminal(self: *Window) bool {
return self.app.quick_terminal == self;
}
pub fn updateConfig(
self: *Window,
config: *const configpkg.Config,
@ -421,7 +429,7 @@ pub fn syncAppearance(self: *Window) !void {
if (!csd_enabled) break :visible false;
// Never display the header bar as a quick terminal.
if (self.app.quick_terminal == self) break :visible false;
if (self.isQuickTerminal()) break :visible false;
// Unconditionally disable the header bar when fullscreened.
if (self.config.fullscreen) break :visible false;
@ -472,12 +480,6 @@ pub fn syncAppearance(self: *Window) !void {
self.winproto.syncAppearance() catch |err| {
log.warn("failed to sync winproto appearance error={}", .{err});
};
if (self.app.quick_terminal == self) {
self.winproto.syncQuickTerminal() catch |err| {
log.warn("failed to sync quick terminal appearance error={}", .{err});
};
}
}
fn toggleCssClass(
@ -730,6 +732,20 @@ fn gtkWindowNotifyFullscreened(
};
}
fn gtkWindowNotifyIsActive(
_: *c.GObject,
_: *c.GParamSpec,
ud: ?*anyopaque,
) callconv(.C) void {
const self = userdataSelf(ud orelse return);
if (!self.isQuickTerminal()) return;
// Hide when we're unfocused
if (self.config.quick_terminal_autohide and c.gtk_window_is_active(self.window) == 0) {
self.toggleVisibility();
}
}
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
// sends an undefined value.
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
@ -803,7 +819,7 @@ pub fn close(self: *Window) void {
const window: *gtk.Window = @ptrCast(self.window);
// Unset the quick terminal on the app level
if (self.app.quick_terminal == self) self.app.quick_terminal = null;
if (self.isQuickTerminal()) self.app.quick_terminal = null;
window.destroy();
}
@ -813,9 +829,6 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
log.debug("window close request", .{});
const self = userdataSelf(ud.?);
// This path should never occur, but this is here as a safety measure.
if (self.app.quick_terminal == self) return true;
// If none of our surfaces need confirmation, we can just exit.
for (self.app.core_app.surfaces.items) |surface| {
if (surface.container.window()) |window| {

View File

@ -133,12 +133,6 @@ pub const Window = union(Protocol) {
}
}
pub fn syncQuickTerminal(self: *Window) !void {
switch (self.*) {
inline else => |*v| try v.syncQuickTerminal(),
}
}
pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self) {
inline else => |v| v.clientSideDecorationEnabled(),

View File

@ -59,8 +59,6 @@ pub const Window = struct {
pub fn syncAppearance(_: *Window) !void {}
pub fn syncQuickTerminal(_: *Window) !void {}
/// This returns true if CSD is enabled for this window. This
/// should be the actual present state of the window, not the
/// desired state.

View File

@ -27,6 +27,8 @@ pub const App = struct {
// 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,
};
@ -117,13 +119,26 @@ pub const App = struct {
global,
)) |blur_manager| {
context.kde_blur_manager = blur_manager;
} else if (registryBind(
return;
}
if (registryBind(
org.KdeKwinServerDecorationManager,
registry,
global,
)) |deco_manager| {
context.kde_decoration_manager = deco_manager;
deco_manager.setListener(*Context, decoManagerListener, context);
return;
}
if (registryBind(
org.KdeKwinSlideManager,
registry,
global,
)) |slide_manager| {
context.kde_slide_manager = slide_manager;
return;
}
},
@ -188,6 +203,10 @@ pub const Window = struct {
/// of the window.
decoration: ?*org.KdeKwinServerDecoration,
/// Object that controls the slide-in/slide-out animations of the
/// quick terminal. Always null for windows other than the quick terminal.
slide: ?*org.KdeKwinSlide,
pub fn init(
alloc: Allocator,
app: *App,
@ -232,6 +251,7 @@ pub const Window = struct {
.app_context = app.context,
.blur_token = null,
.decoration = deco,
.slide = null,
};
}
@ -239,6 +259,7 @@ pub const Window = struct {
_ = alloc;
if (self.blur_token) |blur| blur.release();
if (self.decoration) |deco| deco.release();
if (self.slide) |slide| slide.release();
}
pub fn resizeEvent(_: *Window) !void {}
@ -250,6 +271,12 @@ pub const Window = struct {
self.syncDecoration() catch |err| {
log.err("failed to sync blur={}", .{err});
};
if (self.apprt_window.isQuickTerminal()) {
self.syncQuickTerminal() catch |err| {
log.warn("failed to sync quick terminal appearance={}", .{err});
};
}
}
pub fn clientSideDecorationEnabled(self: Window) bool {
@ -308,7 +335,7 @@ pub const Window = struct {
};
}
pub fn syncQuickTerminal(self: *Window) !void {
fn syncQuickTerminal(self: *Window) !void {
if (comptime !build_options.layer_shell) return;
const window = self.apprt_window.window;
@ -339,6 +366,22 @@ pub const Window = struct {
.top, .bottom, .center => c.gtk_window_set_default_size(window, 800, 400),
.left, .right => c.gtk_window_set_default_size(window, 400, 800),
}
if (self.apprt_window.isQuickTerminal()) {
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 slide = mgr.create(self.surface) catch |err| {
log.warn("could not create slide object={}", .{err});
break :slide null;
};
slide.setLocation(@intCast(@intFromEnum(anchored.toKdeSlideLocation())));
slide.commit();
break :slide slide;
} else null;
}
}
};
@ -347,4 +390,13 @@ const LayerShellEdge = enum(c_uint) {
right = c.GTK_LAYER_SHELL_EDGE_RIGHT,
top = c.GTK_LAYER_SHELL_EDGE_TOP,
bottom = c.GTK_LAYER_SHELL_EDGE_BOTTOM,
fn toKdeSlideLocation(self: LayerShellEdge) org.KdeKwinSlide.Location {
return switch (self) {
.left => .left,
.top => .top,
.right => .right,
.bottom => .bottom,
};
}
};

View File

@ -228,8 +228,6 @@ pub const Window = struct {
};
}
pub fn syncQuickTerminal(_: *Window) !void {}
pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self.config.window_decoration) {
.auto, .client => true,

View File

@ -474,10 +474,12 @@ pub fn add(
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/server-decoration.xml"));
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/slide.xml"));
scanner.generate("wl_compositor", 1);
scanner.generate("org_kde_kwin_blur_manager", 1);
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
scanner.generate("org_kde_kwin_slide_manager", 1);
step.root_module.addImport("wayland", wayland);
step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4"));

View File

@ -1637,7 +1637,8 @@ keybind: Keybinds = .{},
/// * `right` - Terminal appears at the right of the screen.
/// * `center` - Terminal appears at the center of the screen.
///
/// Changing this configuration requires restarting Ghostty completely.
/// On macOS, changing this configuration requires restarting Ghostty
/// completely.
///
/// Note: There is no default keybind for toggling the quick terminal.
/// To enable this feature, bind the `toggle_quick_terminal` action to a key.
@ -1661,11 +1662,15 @@ keybind: Keybinds = .{},
///
/// The default value is `main` because this is the recommended screen
/// by the operating system.
///
/// Only implemented on macOS.
@"quick-terminal-screen": QuickTerminalScreen = .main,
/// Duration (in seconds) of the quick terminal enter and exit animation.
/// Set it to 0 to disable animation completely. This can be changed at
/// runtime.
///
/// Only implemented on macOS.
@"quick-terminal-animation-duration": f64 = 0.2,
/// Automatically hide the quick terminal when focus shifts to another window.
@ -1687,6 +1692,9 @@ keybind: Keybinds = .{},
/// space.
///
/// The default value is `move`.
///
/// Only implemented on macOS.
/// On Linux the behavior is always equivalent to `move`.
@"quick-terminal-space-behavior": QuickTerminalSpaceBehavior = .move,
/// Whether to enable shell integration auto-injection or not. Shell integration

View File

@ -471,7 +471,15 @@ pub const Action = union(enum) {
/// See the various configurations for the quick terminal in the
/// configuration file to customize its behavior.
///
/// This currently only works on macOS.
/// Supported on macOS and some desktop environments on Linux, namely
/// those that support the `wlr-layer-shell` Wayland protocol
/// (i.e. most desktop environments and window managers except GNOME).
///
/// Slide-in animations on Linux are only supported on KDE when the
/// "Sliding Popups" KWin plugin is enabled. If you do not have this
/// plugin enabled, open System Settings > Apps & Windows > Window
/// Management > Desktop Effects, and enable the plugin in the plugin list.
/// Ghostty would then need to be restarted for this to take effect.
toggle_quick_terminal: void,
/// Show/hide all windows. If all windows become shown, we also ensure