From ccde429bde0c36dc0725cdddd75fc89ed8b111bd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 26 Jul 2025 12:29:07 -0700 Subject: [PATCH] apprt/gtk-ng: toasts --- src/apprt/gtk-ng/class/surface.zig | 7 +++-- src/apprt/gtk-ng/class/window.zig | 42 +++++++++++++++++++++++++++++- src/apprt/gtk-ng/ui/1.5/window.blp | 11 +++++--- src/apprt/structs.zig | 19 +++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 7d162b4e9..0cefebc63 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -275,7 +275,10 @@ pub const Surface = extern struct { const impl = gobject.ext.defineSignal( name, Self, - &.{}, + &.{ + apprt.Clipboard, + [*:0]const u8, + }, void, ); }; @@ -2236,7 +2239,7 @@ const Clipboard = struct { Surface.signals.@"clipboard-write".impl.emit( self, null, - .{}, + .{ clipboard_type, val.ptr }, null, ); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 875fad9b6..ac5555391 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -8,6 +8,7 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); const i18n = @import("../../../os/main.zig").i18n; +const apprt = @import("../../../apprt.zig"); const input = @import("../../../input.zig"); const CoreSurface = @import("../../../Surface.zig"); const gtk_version = @import("../gtk_version.zig"); @@ -122,7 +123,8 @@ pub const Window = extern struct { config: ?*Config = null, // Template bindings - surface: *Surface = undefined, + surface: *Surface, + toast_overlay: *adw.ToastOverlay, pub var offset: c_int = 0; }; @@ -227,6 +229,18 @@ pub const Window = extern struct { }; } + /// Queue a simple text-based toast. All text-based toasts share the + /// same timeout for consistency. + /// + // This is not `pub` because we should be using signals emitted by + // other widgets to trigger our toasts. Other objects should not + // trigger toasts directly. + fn addToast(self: *Window, title: [*:0]const u8) void { + const toast = adw.Toast.new(title); + toast.setTimeout(3); + self.private().toast_overlay.addToast(toast); + } + //--------------------------------------------------------------- // Properties @@ -264,6 +278,7 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { + self.addToast(i18n._("Reloaded the configuration")); self.syncAppearance(); } @@ -377,6 +392,29 @@ pub const Window = extern struct { self.as(gtk.Window).destroy(); } + fn surfaceClipboardWrite( + _: *Surface, + clipboard_type: apprt.Clipboard, + text: [*:0]const u8, + self: *Self, + ) callconv(.c) void { + // We only toast for the standard clipboard. + if (clipboard_type != .standard) return; + + // We only toast if configured to + const priv = self.private(); + const config_obj = priv.config orelse return; + const config = config_obj.get(); + if (!config.@"app-notifications".@"clipboard-copy") { + return; + } + + if (text[0] != 0) + self.addToast(i18n._("Copied to clipboard")) + else + self.addToast(i18n._("Cleared clipboard")); + } + fn surfaceCloseRequest( surface: *Surface, scope: *const Surface.CloseScope, @@ -542,9 +580,11 @@ pub const Window = extern struct { // Bindings class.bindTemplateChildPrivate("surface", .{}); + class.bindTemplateChildPrivate("toast_overlay", .{}); // Template Callbacks class.bindTemplateCallback("close_request", &windowCloseRequest); + class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite); class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen); class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index c8ffdade3..1e2345d95 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -43,10 +43,13 @@ template $GhosttyWindow: Adw.ApplicationWindow { visible: bind template.debug; } - $GhosttySurface surface { - close-request => $surface_close_request(); - toggle-fullscreen => $surface_toggle_fullscreen(); - toggle-maximize => $surface_toggle_maximize(); + Adw.ToastOverlay toast_overlay { + $GhosttySurface surface { + close-request => $surface_close_request(); + clipboard-write => $surface_clipboard_write(); + toggle-fullscreen => $surface_toggle_fullscreen(); + toggle-maximize => $surface_toggle_maximize(); + } } }; } diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 1c3b28723..c9948f3ee 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -29,10 +29,27 @@ pub const IMEPos = struct { /// The clipboard type. /// /// If this is changed, you must also update ghostty.h -pub const Clipboard = enum(u2) { +pub const Clipboard = enum(Backing) { standard = 0, // ctrl+c/v selection = 1, primary = 2, + + // Our backing isn't is as small as we can in Zig, but a full + // C int if we're binding to C APIs. + const Backing = switch (build_config.app_runtime) { + .gtk, .@"gtk-ng" => c_int, + else => u2, + }; + + /// Make this a valid gobject if we're in a GTK environment. + pub const getGObjectType = switch (build_config.app_runtime) { + .gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum( + Clipboard, + .{ .name = "GhosttyApprtClipboard" }, + ), + + .none => void, + }; }; pub const ClipboardRequestType = enum(u8) {