diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig index 181259ee7..427789560 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -114,6 +114,34 @@ pub fn Common( ); } }).bindTemplateChildPrivate else {}; + + /// Bind a function pointer to a template callback symbol. + pub fn bindTemplateCallback( + class: *Self.Class, + comptime name: [:0]const u8, + comptime func: anytype, + ) void { + { + const ptr_ti = @typeInfo(@TypeOf(func)); + if (ptr_ti != .pointer) { + @compileError("bound function must be a pointer type"); + } + if (ptr_ti.pointer.size != .one) { + @compileError("bound function must be a pointer to a function"); + } + + const func_ti = @typeInfo(ptr_ti.pointer.child); + if (func_ti != .@"fn") { + @compileError("bound function must be a function pointer"); + } + } + + gtk.Widget.Class.bindTemplateCallbackFull( + class.as(gtk.Widget.Class), + name, + @ptrCast(func), + ); + } }; }; } diff --git a/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig b/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig index 4814909b3..7769d8935 100644 --- a/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig +++ b/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig @@ -163,40 +163,6 @@ pub const ClipboardConfirmationDialog = extern struct { fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); - const priv = self.private(); - - // Signals - _ = gtk.Button.signals.clicked.connect( - priv.reveal_button, - *Self, - revealButtonClicked, - self, - .{}, - ); - _ = gtk.Button.signals.clicked.connect( - priv.hide_button, - *Self, - hideButtonClicked, - self, - .{}, - ); - - // Some property signals - _ = gobject.Object.signals.notify.connect( - self, - ?*anyopaque, - &propBlur, - null, - .{ .detail = "blur" }, - ); - _ = gobject.Object.signals.notify.connect( - self, - ?*anyopaque, - &propRequest, - null, - .{ .detail = "request" }, - ); - // Trigger initial values self.propBlur(undefined, null); self.propRequest(undefined, null); @@ -374,6 +340,12 @@ pub const ClipboardConfirmationDialog = extern struct { class.bindTemplateChildPrivate("remember_choice", .{}); } + // Template Callbacks + class.bindTemplateCallback("reveal_clicked", &revealButtonClicked); + class.bindTemplateCallback("hide_clicked", &hideButtonClicked); + class.bindTemplateCallback("notify_blur", &propBlur); + class.bindTemplateCallback("notify_request", &propRequest); + // Properties gobject.ext.registerProperties(class, &.{ properties.blur.impl, @@ -394,5 +366,6 @@ pub const ClipboardConfirmationDialog = extern struct { pub const as = C.Class.as; pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; }; }; diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 9a64cd101..591a1ff5e 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -245,10 +245,6 @@ pub const Surface = extern struct { /// focus events. focused: bool = true, - /// The overlay we use for things such as the URL hover label - /// or resize box. Bound from the template. - overlay: *gtk.Overlay, - /// The GLAarea that renders the actual surface. This is a binding /// to the template so it doesn't have to be unrefed manually. gl_area: *gtk.GLArea, @@ -256,18 +252,10 @@ pub const Surface = extern struct { /// The labels for the left/right sides of the URL hover tooltip. url_left: *gtk.Label, url_right: *gtk.Label, - url_ec_motion: *gtk.EventControllerMotion, /// The resize overlay resize_overlay: *ResizeOverlay, - // Event controllers - ec_focus: *gtk.EventControllerFocus, - ec_key: *gtk.EventControllerKey, - ec_motion: *gtk.EventControllerMotion, - ec_scroll: *gtk.EventControllerScroll, - gesture_click: *gtk.GestureClick, - /// The apprt Surface. rt_surface: ApprtSurface = undefined, @@ -288,7 +276,7 @@ pub const Surface = extern struct { /// Various input method state. All related to key input. in_keyevent: IMKeyEvent = .false, - im_context: ?*gtk.IMMulticontext = null, + im_context: *gtk.IMMulticontext, im_composing: bool = false, im_buf: [128]u8 = undefined, im_len: u7 = 0, @@ -368,13 +356,13 @@ pub const Surface = extern struct { // The block below is all related to input method handling. See the function // comment for some high level details and then the comments within // the block for more specifics. - if (priv.im_context) |im_context| { + { // This can trigger an input method so we need to notify the im context // where the cursor is so it can render the dropdowns in the correct // place. if (priv.core_surface) |surface| { const ime_point = surface.imePoint(); - im_context.as(gtk.IMContext).setCursorLocation(&.{ + priv.im_context.as(gtk.IMContext).setCursorLocation(&.{ .f_x = @intFromFloat(ime_point.x), .f_y = @intFromFloat(ime_point.y), .f_width = 1, @@ -415,7 +403,7 @@ pub const Surface = extern struct { // triggered despite being technically consumed. At the time of // writing, both Kitty and Alacritty have the same behavior. I // know of no way to fix this. - const im_handled = im_context.as(gtk.IMContext).filterKeypress(event) != 0; + const im_handled = priv.im_context.as(gtk.IMContext).filterKeypress(event) != 0; // log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{ // im_handled, // self.im_len, @@ -546,10 +534,7 @@ pub const Surface = extern struct { // because there is other IME state that we want to preserve, // such as quotation mark ordering for Chinese input. if (priv.im_composing) { - if (priv.im_context) |im_context| { - im_context.as(gtk.IMContext).reset(); - } - + priv.im_context.as(gtk.IMContext).reset(); surface.preeditCallback(null) catch {}; } @@ -803,238 +788,30 @@ pub const Surface = extern struct { priv.config = app.getConfig(); } - // Setup our event controllers to get input events - _ = gtk.EventControllerKey.signals.key_pressed.connect( - priv.ec_key, - *Self, - ecKeyPressed, - self, - .{}, - ); - _ = gtk.EventControllerKey.signals.key_released.connect( - priv.ec_key, - *Self, - ecKeyReleased, - self, - .{}, - ); - - // Focus controller will tell us about focus enter/exit events - _ = gtk.EventControllerFocus.signals.enter.connect( - priv.ec_focus, - *Self, - ecFocusEnter, - self, - .{}, - ); - _ = gtk.EventControllerFocus.signals.leave.connect( - priv.ec_focus, - *Self, - ecFocusLeave, - self, - .{}, - ); - - // Clicks - _ = gtk.GestureClick.signals.pressed.connect( - priv.gesture_click, - *Self, - gcMouseDown, - self, - .{}, - ); - _ = gtk.GestureClick.signals.released.connect( - priv.gesture_click, - *Self, - gcMouseUp, - self, - .{}, - ); - - // Mouse movement - _ = gtk.EventControllerMotion.signals.motion.connect( - priv.ec_motion, - *Self, - ecMouseMotion, - self, - .{}, - ); - _ = gtk.EventControllerMotion.signals.leave.connect( - priv.ec_motion, - *Self, - ecMouseLeave, - self, - .{}, - ); - - // Scroll - _ = gtk.EventControllerScroll.signals.scroll.connect( - priv.ec_scroll, - *Self, - ecMouseScroll, - self, - .{}, - ); - _ = gtk.EventControllerScroll.signals.scroll_begin.connect( - priv.ec_scroll, - *Self, - ecMouseScrollPrecisionBegin, - self, - .{}, - ); - _ = gtk.EventControllerScroll.signals.scroll_end.connect( - priv.ec_scroll, - *Self, - ecMouseScrollPrecisionEnd, - self, - .{}, - ); - // Setup our input method state - const im_context = gtk.IMMulticontext.new(); - priv.im_context = im_context; priv.in_keyevent = .false; priv.im_composing = false; priv.im_len = 0; - _ = gtk.IMContext.signals.preedit_start.connect( - im_context, - *Self, - imPreeditStart, - self, - .{}, - ); - _ = gtk.IMContext.signals.preedit_changed.connect( - im_context, - *Self, - imPreeditChanged, - self, - .{}, - ); - _ = gtk.IMContext.signals.preedit_end.connect( - im_context, - *Self, - imPreeditEnd, - self, - .{}, - ); - _ = gtk.IMContext.signals.commit.connect( - im_context, - *Self, - imCommit, - self, - .{}, - ); - // Initialize our GLArea. We could do a lot of this in - // the Blueprint file but I think its cleaner to separate - // the "UI" part of the blueprint file from the internal logic/config - // part. + // Initialize our GLArea. We only set the values we can't set + // in our blueprint file. const gl_area = priv.gl_area; gl_area.setRequiredVersion( renderer.OpenGL.MIN_VERSION_MAJOR, renderer.OpenGL.MIN_VERSION_MINOR, ); - gl_area.setHasStencilBuffer(0); - gl_area.setHasDepthBuffer(0); - gl_area.setUseEs(0); gl_area.as(gtk.Widget).setCursorFromName("text"); - _ = gtk.Widget.signals.realize.connect( - gl_area, - *Self, - glareaRealize, - self, - .{}, - ); - _ = gtk.Widget.signals.unrealize.connect( - gl_area, - *Self, - glareaUnrealize, - self, - .{}, - ); - _ = gtk.GLArea.signals.render.connect( - gl_area, - *Self, - glareaRender, - self, - .{}, - ); - _ = gtk.GLArea.signals.resize.connect( - gl_area, - *Self, - glareaResize, - self, - .{}, - ); - - // Some property signals - _ = gobject.Object.signals.notify.connect( - self, - ?*anyopaque, - &propConfig, - null, - .{ .detail = "config" }, - ); - _ = gobject.Object.signals.notify.connect( - self, - ?*anyopaque, - &propMouseHoverUrl, - null, - .{ .detail = "mouse-hover-url" }, - ); - _ = gobject.Object.signals.notify.connect( - self, - ?*anyopaque, - &propMouseHidden, - null, - .{ .detail = "mouse-hidden" }, - ); - _ = gobject.Object.signals.notify.connect( - self, - ?*anyopaque, - &propMouseShape, - null, - .{ .detail = "mouse-shape" }, - ); - - // Some other initialization steps - self.initUrlOverlay(); // Initialize our config self.propConfig(undefined, null); } - fn initUrlOverlay(self: *Self) void { - const priv = self.private(); - - // Setup a motion controller to handle moving the label - // to avoid the mouse. - _ = gtk.EventControllerMotion.signals.enter.connect( - priv.url_ec_motion, - *Self, - ecUrlMouseEnter, - self, - .{}, - ); - _ = gtk.EventControllerMotion.signals.leave.connect( - priv.url_ec_motion, - *Self, - ecUrlMouseLeave, - self, - .{}, - ); - } - fn dispose(self: *Self) callconv(.C) void { const priv = self.private(); if (priv.config) |v| { v.unref(); priv.config = null; } - if (priv.im_context) |v| { - v.unref(); - priv.im_context = null; - } gtk.Widget.disposeTemplate( self.as(gtk.Widget), @@ -1260,22 +1037,14 @@ pub const Surface = extern struct { fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { const priv = self.private(); priv.focused = true; - - if (priv.im_context) |im_context| { - im_context.as(gtk.IMContext).focusIn(); - } - + priv.im_context.as(gtk.IMContext).focusIn(); _ = glib.idleAddOnce(idleFocus, self.ref()); } fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { const priv = self.private(); priv.focused = false; - - if (priv.im_context) |im_context| { - im_context.as(gtk.IMContext).focusOut(); - } - + priv.im_context.as(gtk.IMContext).focusOut(); _ = glib.idleAddOnce(idleFocus, self.ref()); } @@ -1647,9 +1416,8 @@ pub const Surface = extern struct { // Setup our input method. We do this here because this will // create a strong reference back to ourself and we want to be // able to release that in unrealize. - if (self.private().im_context) |im_context| { - im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget)); - } + const priv = self.private(); + priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget)); } fn glareaUnrealize( @@ -1683,9 +1451,7 @@ pub const Surface = extern struct { } // Unset our input method - if (priv.im_context) |im_context| { - im_context.as(gtk.IMContext).setClientWidget(null); - } + priv.im_context.as(gtk.IMContext).setClientWidget(null); } fn glareaRender( @@ -1899,29 +1665,38 @@ pub const Surface = extern struct { ); // Bindings - class.bindTemplateChildPrivate("overlay", .{}); class.bindTemplateChildPrivate("gl_area", .{}); class.bindTemplateChildPrivate("url_left", .{}); class.bindTemplateChildPrivate("url_right", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); + class.bindTemplateChildPrivate("im_context", .{}); - // EventControllers don't work with our helper. - // https://github.com/ianprime0509/zig-gobject/issues/111 - inline for (&.{ - "ec_focus", - "ec_key", - "ec_motion", - "ec_scroll", - "gesture_click", - "url_ec_motion", - }) |name| { - gtk.Widget.Class.bindTemplateChildFull( - gobject.ext.as(gtk.Widget.Class, class), - name, - @intFromBool(false), - Private.offset + @offsetOf(Private, name), - ); - } + // Template Callbacks + class.bindTemplateCallback("focus_enter", &ecFocusEnter); + class.bindTemplateCallback("focus_leave", &ecFocusLeave); + class.bindTemplateCallback("key_pressed", &ecKeyPressed); + class.bindTemplateCallback("key_released", &ecKeyReleased); + class.bindTemplateCallback("mouse_down", &gcMouseDown); + class.bindTemplateCallback("mouse_up", &gcMouseUp); + class.bindTemplateCallback("mouse_motion", &ecMouseMotion); + class.bindTemplateCallback("mouse_leave", &ecMouseLeave); + class.bindTemplateCallback("scroll", &ecMouseScroll); + class.bindTemplateCallback("scroll_begin", &ecMouseScrollPrecisionBegin); + class.bindTemplateCallback("scroll_end", &ecMouseScrollPrecisionEnd); + class.bindTemplateCallback("gl_realize", &glareaRealize); + class.bindTemplateCallback("gl_unrealize", &glareaUnrealize); + class.bindTemplateCallback("gl_render", &glareaRender); + class.bindTemplateCallback("gl_resize", &glareaResize); + class.bindTemplateCallback("im_preedit_start", &imPreeditStart); + class.bindTemplateCallback("im_preedit_changed", &imPreeditChanged); + class.bindTemplateCallback("im_preedit_end", &imPreeditEnd); + class.bindTemplateCallback("im_commit", &imCommit); + class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter); + class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave); + class.bindTemplateCallback("notify_config", &propConfig); + class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); + class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); + class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); // Properties gobject.ext.registerProperties(class, &.{ @@ -1946,6 +1721,7 @@ pub const Surface = extern struct { pub const as = C.Class.as; pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; }; }; diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 6e30389a0..82bf6b247 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -36,15 +36,6 @@ pub const Window = extern struct { fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); - - const surface = self.private().surface; - _ = Surface.signals.@"close-request".connect( - surface, - *Self, - surfaceCloseRequest, - self, - .{}, - ); } //--------------------------------------------------------------- @@ -102,11 +93,15 @@ pub const Window = extern struct { // Bindings class.bindTemplateChildPrivate("surface", .{}); + // Template Callbacks + class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); + // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); } pub const as = C.Class.as; pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; }; }; diff --git a/src/apprt/gtk-ng/ui/1.0/clipboard-confirmation-dialog.blp b/src/apprt/gtk-ng/ui/1.0/clipboard-confirmation-dialog.blp index c3b760373..5f29981df 100644 --- a/src/apprt/gtk-ng/ui/1.0/clipboard-confirmation-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.0/clipboard-confirmation-dialog.blp @@ -7,6 +7,8 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { "clipboard-confirmation-dialog", ] + notify::blur => $notify_blur(); + notify::request => $notify_request(); heading: _("Authorize Clipboard Access"); // Not localized because this is a placeholder users never see. body: "If you see this text, there is a bug in Ghostty. Please report it."; @@ -50,6 +52,7 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { [overlay] Button reveal_button { + clicked => $reveal_clicked(); visible: false; halign: end; valign: start; @@ -63,6 +66,7 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { [overlay] Button hide_button { + clicked => $hide_clicked(); visible: false; halign: end; valign: start; diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 3d17b1d56..e18b4d85d 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -2,66 +2,98 @@ using Gtk 4.0; using Adw 1; template $GhosttySurface: Adw.Bin { - // We need to wrap our Overlay one more time because if you bind a - // direct child of your widget to a property, it will double free: - // https://gitlab.gnome.org/GNOME/gtk/-/blob/847571a1e314aba79260e4ef282e2ed9ba91a0d9/gtk/gtkwidget.c#L11423-11425 - Adw.Bin { - Overlay overlay { - focusable: false; - focus-on-click: false; + notify::config => $notify_config(); + notify::mouse-hover-url => $notify_mouse_hover_url(); + notify::mouse-hidden => $notify_mouse_hidden(); + notify::mouse-shape => $notify_mouse_shape(); - GLArea gl_area { - hexpand: true; - vexpand: true; - focusable: true; - focus-on-click: true; + Overlay { + focusable: false; + focus-on-click: false; + + GLArea gl_area { + realize => $gl_realize(); + unrealize => $gl_unrealize(); + render => $gl_render(); + resize => $gl_resize(); + hexpand: true; + vexpand: true; + focusable: true; + focus-on-click: true; + has-stencil-buffer: false; + has-depth-buffer: false; + use-es: false; + } + + [overlay] + $GhosttyResizeOverlay resize_overlay { + styles [ + "size-overlay", + ] + } + + [overlay] + Label url_left { + styles [ + "url-overlay", + ] + + visible: false; + halign: start; + valign: end; + label: bind template.mouse-hover-url; + + EventControllerMotion url_ec_motion { + enter => $url_mouse_enter(); + leave => $url_mouse_leave(); } + } - [overlay] - $GhosttyResizeOverlay resize_overlay { - styles [ - "size-overlay", - ] - } + [overlay] + Label url_right { + styles [ + "url-overlay", + ] - [overlay] - Label url_left { - styles [ - "url-overlay", - ] - - visible: false; - halign: start; - valign: end; - label: bind template.mouse-hover-url; - - EventControllerMotion url_ec_motion {} - } - - [overlay] - Label url_right { - styles [ - "url-overlay", - ] - - visible: false; - halign: end; - valign: end; - label: bind template.mouse-hover-url; - } + visible: false; + halign: end; + valign: end; + label: bind template.mouse-hover-url; } } // Event controllers for interactivity - EventControllerFocus ec_focus {} + EventControllerFocus { + enter => $focus_enter(); + leave => $focus_leave(); + } - EventControllerKey ec_key {} + EventControllerKey { + key-pressed => $key_pressed(); + key-released => $key_released(); + } - EventControllerMotion ec_motion {} + EventControllerMotion { + motion => $mouse_motion(); + leave => $mouse_leave(); + } - EventControllerScroll ec_scroll {} + EventControllerScroll { + scroll => $scroll(); + scroll-begin => $scroll_begin(); + scroll-end => $scroll_end(); + } - GestureClick gesture_click { + GestureClick { + pressed => $mouse_down(); + released => $mouse_up(); button: 0; } } + +IMMulticontext im_context { + preedit-start => $im_preedit_start(); + preedit-changed => $im_preedit_changed(); + preedit-end => $im_preedit_end(); + commit => $im_commit(); +} diff --git a/src/apprt/gtk-ng/ui/1.4/clipboard-confirmation-dialog.blp b/src/apprt/gtk-ng/ui/1.4/clipboard-confirmation-dialog.blp index 066927ff2..144e3a371 100644 --- a/src/apprt/gtk-ng/ui/1.4/clipboard-confirmation-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.4/clipboard-confirmation-dialog.blp @@ -7,6 +7,8 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { "clipboard-confirmation-dialog", ] + notify::blur => $notify_blur(); + notify::request => $notify_request(); heading: _("Authorize Clipboard Access"); // Not localized because this is a placeholder users never see. body: "If you see this text, there is a bug in Ghostty. Please report it."; @@ -50,6 +52,7 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { [overlay] Button reveal_button { + clicked => $reveal_clicked(); visible: false; halign: end; valign: start; @@ -63,6 +66,7 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { [overlay] Button hide_button { + clicked => $hide_clicked(); visible: false; halign: end; valign: start; diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 930ee53db..df7df8e61 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -5,5 +5,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { default-width: 800; default-height: 600; - content: $GhosttySurface surface {}; + content: $GhosttySurface surface { + close-request => $surface_close_request(); + }; }