diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index b56d91c9e..a031998ce 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -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(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(self.window, "notify::is-active", c.G_CALLBACK(>kWindowNotifyIsActive), 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| { diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index e99a5fb2b..01587a226 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -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(), diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index d030d884e..c71394e7a 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -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. diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index f9ea7c7de..e27b6bf77 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -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, + }; + } }; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 6d20e6e8b..c8f5385dd 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -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, diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 26c4d84c5..7b2d10c63 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -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")); diff --git a/src/config/Config.zig b/src/config/Config.zig index c2749b45c..a6612a4d3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 diff --git a/src/input/Binding.zig b/src/input/Binding.zig index ddee672a5..b684e6cd6 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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