From 572fc8b5d78310d86cda493b241bc17714f7d44b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 8 Mar 2025 23:19:16 -0600 Subject: [PATCH] gtk: convert Surface to zig-gobject --- src/apprt/gtk/App.zig | 11 +- src/apprt/gtk/ImguiWidget.zig | 3 +- src/apprt/gtk/Surface.zig | 822 +++++++++++++++++------------ src/apprt/gtk/Tab.zig | 8 +- src/apprt/gtk/Window.zig | 27 +- src/apprt/gtk/key.zig | 80 ++- src/apprt/gtk/winproto.zig | 7 +- src/apprt/gtk/winproto/noop.zig | 7 +- src/apprt/gtk/winproto/wayland.zig | 6 +- src/apprt/gtk/winproto/x11.zig | 7 +- src/build/SharedDeps.zig | 6 +- 11 files changed, 565 insertions(+), 419 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 2ed90eccf..cc33fd5e4 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -10,11 +10,12 @@ /// (event loop) along with any global app state. const App = @This(); -const gtk = @import("gtk"); +const adw = @import("adw"); +const gdk = @import("gdk"); const gio = @import("gio"); const glib = @import("glib"); const gobject = @import("gobject"); -const adw = @import("adw"); +const gtk = @import("gtk"); const std = @import("std"); const assert = std.debug.assert; @@ -64,7 +65,7 @@ winproto: winprotopkg.App, single_instance: bool, /// The "none" cursor. We use one that is shared across the entire app. -cursor_none: ?*c.GdkCursor, +cursor_none: ?*gdk.Cursor, /// The configuration errors window, if it is currently open. config_errors_window: ?*ConfigErrorsWindow = null, @@ -280,8 +281,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { }; // The "none" cursor is used for hiding the cursor - const cursor_none = c.gdk_cursor_new_from_name("none", null); - errdefer if (cursor_none) |cursor| c.g_object_unref(cursor); + const cursor_none = gdk.Cursor.newFromName("none", null); + errdefer if (cursor_none) |cursor| cursor.unref(); const single_instance = switch (config.@"gtk-single-instance") { .true => true, diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig index 1f42f0b49..735629341 100644 --- a/src/apprt/gtk/ImguiWidget.zig +++ b/src/apprt/gtk/ImguiWidget.zig @@ -370,8 +370,9 @@ fn keyEvent( cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + // FIXME: when this file get converted to zig-gobject // Translate the GTK mods and update the modifiers on every keypress - const mods = key.translateMods(gtk_mods); + const mods = key.translateMods(@bitCast(gtk_mods)); cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift); cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl); cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 805681941..e96b25c16 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -7,6 +7,8 @@ const std = @import("std"); const adw = @import("adw"); const gtk = @import("gtk"); +const gdk = @import("gdk"); +const glib = @import("glib"); const gio = @import("gio"); const gobject = @import("gobject"); @@ -34,7 +36,6 @@ const URLWidget = @import("URLWidget.zig"); const CloseDialog = @import("CloseDialog.zig"); const inspectorpkg = @import("inspector.zig"); const gtk_key = @import("key.zig"); -const c = @import("c.zig").c; const Builder = @import("Builder.zig"); const adwaita = @import("adwaita.zig"); @@ -85,10 +86,10 @@ pub const Container = union(enum) { /// Returns the GTK widget to add to the paned for the given /// element - pub fn widget(self: Elem) *c.GtkWidget { + pub fn widget(self: Elem) *gtk.Widget { return switch (self) { .surface => |s| s.primaryWidget(), - .split => |s| @ptrCast(@alignCast(s.paned)), + .split => |s| s.paned.as(gtk.Widget), }; } @@ -239,10 +240,10 @@ container: Container = .{ .none = {} }, app: *App, /// The overlay, this is the primary widget -overlay: *c.GtkOverlay, +overlay: *gtk.Overlay, /// Our GTK area -gl_area: *c.GtkGLArea, +gl_area: *gtk.GLArea, /// If non-null this is the widget on the overlay that shows the URL. url_widget: ?URLWidget = null, @@ -254,10 +255,10 @@ resize_overlay: ResizeOverlay = undefined, zoomed_in: bool = false, /// If non-null this is the widget on the overlay which dims the surface when it is unfocused -unfocused_widget: ?*c.GtkWidget = null, +unfocused_widget: ?*gtk.Widget = null, /// Any active cursor we may have -cursor: ?*c.GdkCursor = null, +cursor: ?*gdk.Cursor = null, /// Our title. The raw value of the title. This will be kept up to date and /// .title will be updated if we have focus. @@ -277,7 +278,7 @@ title_from_terminal: ?[:0]const u8 = null, pwd: ?[:0]const u8 = null, /// The timer used to delay title updates in order to prevent flickering. -update_title_timer: ?c.guint = null, +update_title_timer: ?c_uint = null, /// The core surface backing this surface core_surface: CoreSurface, @@ -294,7 +295,7 @@ inspector: ?*inspectorpkg.Inspector = null, /// Key input states. See gtkKeyPressed for detailed descriptions. in_keyevent: IMKeyEvent = .false, -im_context: *c.GtkIMContext, +im_context: *gtk.IMMulticontext, im_composing: bool = false, im_buf: [128]u8 = undefined, im_len: u7 = 0, @@ -364,15 +365,17 @@ pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface { } pub fn init(self: *Surface, app: *App, opts: Options) !void { - const gl_area = c.gtk_gl_area_new(); + const gl_area = gtk.GLArea.new(); + const gl_area_widget = gl_area.as(gtk.Widget); // Create an overlay so we can layer the GL area with other widgets. - const overlay = c.gtk_overlay_new(); - c.gtk_overlay_set_child(@ptrCast(overlay), gl_area); + const overlay = gtk.Overlay.new(); + const overlay_widget = overlay.as(gtk.Widget); + overlay.setChild(gl_area_widget); // Overlay is not focusable, but the GL area is. - c.gtk_widget_set_focusable(@ptrCast(overlay), 0); - c.gtk_widget_set_focus_on_click(@ptrCast(overlay), 0); + overlay_widget.setFocusable(0); + overlay_widget.setFocusOnClick(0); // We grab the floating reference to the primary widget. This allows the // widget tree to be moved around i.e. between a split, a tab, etc. @@ -381,75 +384,81 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { // // This is unref'd in the unref() method that's called by the // self.container through Elem.deinit. - _ = c.g_object_ref_sink(@ptrCast(overlay)); - errdefer c.g_object_unref(@ptrCast(overlay)); + _ = overlay.as(gobject.Object).refSink(); + errdefer overlay.unref(); // We want the gl area to expand to fill the parent container. - c.gtk_widget_set_hexpand(gl_area, 1); - c.gtk_widget_set_vexpand(gl_area, 1); + gl_area_widget.setHexpand(1); + gl_area_widget.setVexpand(1); // Various other GL properties - c.gtk_widget_set_cursor_from_name(@ptrCast(gl_area), "text"); - c.gtk_gl_area_set_required_version(@ptrCast(gl_area), 3, 3); - c.gtk_gl_area_set_has_stencil_buffer(@ptrCast(gl_area), 0); - c.gtk_gl_area_set_has_depth_buffer(@ptrCast(gl_area), 0); - c.gtk_gl_area_set_use_es(@ptrCast(gl_area), 0); + gl_area_widget.setCursorFromName("text"); + gl_area.setRequiredVersion(3, 3); + gl_area.setHasStencilBuffer(0); + gl_area.setHasDepthBuffer(0); + gl_area.setUseEs(0); // Key event controller will tell us about raw keypress events. - const ec_key = c.gtk_event_controller_key_new(); - errdefer c.g_object_unref(ec_key); - c.gtk_widget_add_controller(@ptrCast(overlay), ec_key); - errdefer c.gtk_widget_remove_controller(@ptrCast(overlay), ec_key); + const ec_key = gtk.EventControllerKey.new(); + errdefer ec_key.unref(); + overlay_widget.addController(ec_key.as(gtk.EventController)); + errdefer overlay_widget.removeController(ec_key.as(gtk.EventController)); // Focus controller will tell us about focus enter/exit events - const ec_focus = c.gtk_event_controller_focus_new(); - errdefer c.g_object_unref(ec_focus); - c.gtk_widget_add_controller(@ptrCast(overlay), ec_focus); - errdefer c.gtk_widget_remove_controller(@ptrCast(overlay), ec_focus); + const ec_focus = gtk.EventControllerFocus.new(); + errdefer ec_focus.unref(); + overlay_widget.addController(ec_focus.as(gtk.EventController)); + errdefer overlay_widget.removeController(ec_focus.as(gtk.EventController)); // Create a second key controller so we can receive the raw // key-press events BEFORE the input method gets them. - const ec_key_press = c.gtk_event_controller_key_new(); - errdefer c.g_object_unref(ec_key_press); - c.gtk_widget_add_controller(@ptrCast(overlay), ec_key_press); - errdefer c.gtk_widget_remove_controller(@ptrCast(overlay), ec_key_press); + const ec_key_press = gtk.EventControllerKey.new(); + errdefer ec_key_press.unref(); + overlay_widget.addController(ec_key_press.as(gtk.EventController)); + errdefer overlay_widget.removeController(ec_key_press.as(gtk.EventController)); // Clicks - const gesture_click = c.gtk_gesture_click_new(); - errdefer c.g_object_unref(gesture_click); - c.gtk_gesture_single_set_button(@ptrCast(gesture_click), 0); - c.gtk_widget_add_controller(@ptrCast(@alignCast(overlay)), @ptrCast(gesture_click)); + const gesture_click = gtk.GestureClick.new(); + errdefer gesture_click.unref(); + gesture_click.as(gtk.GestureSingle).setButton(0); + overlay_widget.addController(gesture_click.as(gtk.EventController)); + errdefer overlay_widget.removeController(gesture_click.as(gtk.EventController)); // Mouse movement - const ec_motion = c.gtk_event_controller_motion_new(); - errdefer c.g_object_unref(ec_motion); - c.gtk_widget_add_controller(@ptrCast(@alignCast(overlay)), ec_motion); + const ec_motion = gtk.EventControllerMotion.new(); + errdefer ec_motion.unref(); + overlay_widget.addController(ec_motion.as(gtk.EventController)); + errdefer overlay_widget.removeController(ec_motion.as(gtk.EventController)); // Scroll events - const ec_scroll = c.gtk_event_controller_scroll_new(c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); - errdefer c.g_object_unref(ec_scroll); - c.gtk_widget_add_controller(@ptrCast(overlay), ec_scroll); + const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes); + errdefer ec_scroll.unref(); + overlay_widget.addController(ec_scroll.as(gtk.EventController)); + errdefer overlay_widget.removeController(ec_scroll.as(gtk.EventController)); // The input method context that we use to translate key events into // characters. This doesn't have an event key controller attached because // we call it manually from our own key controller. - const im_context = c.gtk_im_multicontext_new(); - errdefer c.g_object_unref(im_context); + const im_context = gtk.IMMulticontext.new(); + errdefer im_context.unref(); // The GL area has to be focusable so that it can receive events - c.gtk_widget_set_focusable(gl_area, 1); - c.gtk_widget_set_focus_on_click(gl_area, 1); + gl_area_widget.setFocusable(1); + gl_area_widget.setFocusOnClick(1); // Set up to handle items being dropped on our surface. Files can be dropped // from Nautilus and strings can be dropped from many programs. - const drop_target = c.gtk_drop_target_new(c.G_TYPE_INVALID, c.GDK_ACTION_COPY); - errdefer c.g_object_unref(drop_target); - var drop_target_types = [_]c.GType{ - c.gdk_file_list_get_type(), - c.G_TYPE_STRING, + const drop_target = gtk.DropTarget.new(gobject.ext.types.invalid, .flags_copy); + errdefer drop_target.unref(); + // The order of the types matters. + var drop_target_types = [_]gobject.Type{ + gdk.FileList.getGObjectType(), + gio.File.getGObjectType(), + gobject.ext.types.string, }; - c.gtk_drop_target_set_gtypes(drop_target, @ptrCast(&drop_target_types), drop_target_types.len); - c.gtk_widget_add_controller(@ptrCast(overlay), @ptrCast(drop_target)); + drop_target.setGtypes(&drop_target_types, drop_target_types.len); + overlay_widget.addController(drop_target.as(gtk.EventController)); + errdefer overlay_widget.removeController(drop_target.as(gtk.EventController)); // Inherit the parent's font size if we have a parent. const font_size: ?font.face.DesiredSize = font_size: { @@ -496,8 +505,8 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, .container = .{ .none = {} }, - .overlay = @ptrCast(overlay), - .gl_area = @ptrCast(gl_area), + .overlay = overlay, + .gl_area = gl_area, .resize_overlay = undefined, .title_text = null, .core_surface = undefined, @@ -522,28 +531,153 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { try self.setMouseShape(.text); // GL events - _ = c.g_signal_connect_data(gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); - - _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_key_press, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(>kMouseUp), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(>kMouseMotion), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_motion, "leave", c.G_CALLBACK(>kMouseLeave), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(>kMouseScroll), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_scroll, "scroll-begin", c.G_CALLBACK(>kMouseScrollPrecisionBegin), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_scroll, "scroll-end", c.G_CALLBACK(>kMouseScrollPrecisionEnd), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(im_context, "preedit-start", c.G_CALLBACK(>kInputPreeditStart), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(drop_target, "drop", c.G_CALLBACK(>kDrop), self, null, c.G_CONNECT_DEFAULT); + _ = gtk.Widget.signals.realize.connect( + gl_area, + *Surface, + gtkRealize, + self, + .{}, + ); + _ = gtk.Widget.signals.unrealize.connect( + gl_area, + *Surface, + gtkUnrealize, + self, + .{}, + ); + _ = gtk.Widget.signals.destroy.connect( + gl_area, + *Surface, + gtkDestroy, + self, + .{}, + ); + _ = gtk.GLArea.signals.render.connect( + gl_area, + *Surface, + gtkRender, + self, + .{}, + ); + _ = gtk.GLArea.signals.resize.connect( + gl_area, + *Surface, + gtkResize, + self, + .{}, + ); + _ = gtk.EventControllerKey.signals.key_pressed.connect( + ec_key_press, + *Surface, + gtkKeyPressed, + self, + .{}, + ); + _ = gtk.EventControllerKey.signals.key_released.connect( + ec_key_press, + *Surface, + gtkKeyReleased, + self, + .{}, + ); + _ = gtk.EventControllerFocus.signals.enter.connect( + ec_focus, + *Surface, + gtkFocusEnter, + self, + .{}, + ); + _ = gtk.EventControllerFocus.signals.leave.connect( + ec_focus, + *Surface, + gtkFocusLeave, + self, + .{}, + ); + _ = gtk.GestureClick.signals.pressed.connect( + gesture_click, + *Surface, + gtkMouseDown, + self, + .{}, + ); + _ = gtk.GestureClick.signals.released.connect( + gesture_click, + *Surface, + gtkMouseUp, + self, + .{}, + ); + _ = gtk.EventControllerMotion.signals.motion.connect( + ec_motion, + *Surface, + gtkMouseMotion, + self, + .{}, + ); + _ = gtk.EventControllerMotion.signals.leave.connect( + ec_motion, + *Surface, + gtkMouseLeave, + self, + .{}, + ); + _ = gtk.EventControllerScroll.signals.scroll.connect( + ec_scroll, + *Surface, + gtkMouseScroll, + self, + .{}, + ); + _ = gtk.EventControllerScroll.signals.scroll_begin.connect( + ec_scroll, + *Surface, + gtkMouseScrollPrecisionBegin, + self, + .{}, + ); + _ = gtk.EventControllerScroll.signals.scroll_end.connect( + ec_scroll, + *Surface, + gtkMouseScrollPrecisionEnd, + self, + .{}, + ); + _ = gtk.IMContext.signals.preedit_start.connect( + im_context, + *Surface, + gtkInputPreeditStart, + self, + .{}, + ); + _ = gtk.IMContext.signals.preedit_changed.connect( + im_context, + *Surface, + gtkInputPreeditChanged, + self, + .{}, + ); + _ = gtk.IMContext.signals.preedit_end.connect( + im_context, + *Surface, + gtkInputPreeditEnd, + self, + .{}, + ); + _ = gtk.IMContext.signals.commit.connect( + im_context, + *Surface, + gtkInputCommit, + self, + .{}, + ); + _ = gtk.DropTarget.signals.drop.connect( + drop_target, + *Surface, + gtkDrop, + self, + .{}, + ); } fn realize(self: *Surface) !void { @@ -618,9 +752,9 @@ pub fn deinit(self: *Surface) void { // Note we don't do anything with the "unfocused_overlay" because // it is attached to the overlay which by this point has been destroyed // and therefore the unfocused_overlay has been destroyed as well. - c.g_object_unref(self.im_context); - if (self.cursor) |cursor| c.g_object_unref(cursor); - if (self.update_title_timer) |timer| _ = c.g_source_remove(timer); + self.im_context.unref(); + if (self.cursor) |cursor| cursor.unref(); + if (self.update_title_timer) |timer| _ = glib.Source.remove(timer); self.resize_overlay.deinit(); } @@ -632,7 +766,7 @@ pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void { // unref removes the long-held reference to the gl_area and kicks off the // deinit/destroy process for this surface. pub fn unref(self: *Surface) void { - c.g_object_unref(self.overlay); + self.overlay.unref(); } pub fn destroy(self: *Surface, alloc: Allocator) void { @@ -640,8 +774,8 @@ pub fn destroy(self: *Surface, alloc: Allocator) void { alloc.destroy(self); } -pub fn primaryWidget(self: *Surface) *c.GtkWidget { - return @ptrCast(@alignCast(self.overlay)); +pub fn primaryWidget(self: *Surface) *gtk.Widget { + return self.overlay.as(gtk.Widget); } fn render(self: *Surface) !void { @@ -660,7 +794,7 @@ pub fn queueInspectorRender(self: *Surface) void { /// Invalidate the surface so that it forces a redraw on the next tick. pub fn redraw(self: *Surface) void { - c.gtk_gl_area_queue_render(self.gl_area); + self.gl_area.queueRender(); } /// Close this surface. @@ -712,16 +846,6 @@ pub fn controlInspector( }; } -pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { - switch (self.title) { - .none => return null, - .label => |label| { - const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); - return widget; - }, - } -} - pub fn setShouldClose(self: *Surface) void { _ = self; } @@ -733,7 +857,7 @@ pub fn shouldClose(self: *const Surface) bool { pub fn getContentScale(self: *const Surface) !apprt.ContentScale { const gtk_scale: f32 = scale: { - const widget: *gtk.Widget = @ptrCast(@alignCast(self.gl_area)); + const widget = self.gl_area.as(gtk.Widget); // Future: detect GTK version 4.12+ and use gdk_surface_get_scale so we // can support fractional scaling. const scale = widget.getScaleFactor(); @@ -796,13 +920,12 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void const window = self.container.window() orelse return; if (window.notebook.nPages() > 1) return; + // FIXME: when Window is converted to zig-gobject + const gtk_window: *gtk.Window = @ptrCast(@alignCast(window.window)); + // Note: this doesn't properly take into account the window decorations. // I'm not currently sure how to do that. - c.gtk_window_set_default_size( - @ptrCast(window.window), - @intCast(width), - @intCast(height), - ); + gtk_window.setDefaultSize(@intCast(width), @intCast(height)); } pub fn setSizeLimits(self: *const Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { @@ -818,13 +941,12 @@ pub fn setSizeLimits(self: *const Surface, min: apprt.SurfaceSize, max_: ?apprt. const window = self.container.window() orelse return; if (window.notebook.nPages() > 1) return; + // FIXME: when Window is converted to zig-gobject + const widget: *gtk.Widget = @ptrCast(@alignCast(window.window)); + // Note: this doesn't properly take into account the window decorations. // I'm not currently sure how to do that. - c.gtk_widget_set_size_request( - @ptrCast(window.window), - @intCast(min.width), - @intCast(min.height), - ); + widget.setSizeRequest(@intCast(min.width), @intCast(min.height)); } pub fn grabFocus(self: *Surface) void { @@ -839,8 +961,7 @@ pub fn grabFocus(self: *Surface) void { tab.focus_child = self; } - const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); - _ = c.gtk_widget_grab_focus(widget); + _ = self.gl_area.as(gtk.Widget).grabFocus(); self.updateTitleLabels(); } @@ -856,8 +977,8 @@ fn updateTitleLabels(self: *Surface) void { // If we have a window and are focused, then we have to update the window title. if (self.container.window()) |window| { - const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); - if (c.gtk_widget_is_focus(widget) == 1) { + const widget = self.gl_area.as(gtk.Widget); + if (widget.isFocus() != 0) { // Changing the title somehow unhides our cursor. // https://github.com/ghostty-org/ghostty/issues/1419 // I don't know a way around this yet. I've tried re-hiding the @@ -897,21 +1018,21 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo // delay the title update to prevent flickering if (self.update_title_timer) |timer| { - if (c.g_source_remove(timer) == c.FALSE) { + if (glib.Source.remove(timer) == 0) { log.warn("unable to remove update title timer", .{}); } self.update_title_timer = null; } - self.update_title_timer = c.g_timeout_add(75, updateTitleTimerExpired, self); + self.update_title_timer = glib.timeoutAdd(75, updateTitleTimerExpired, self); } -fn updateTitleTimerExpired(ctx: ?*anyopaque) callconv(.C) c.gboolean { - const self: *Surface = @ptrCast(@alignCast(ctx)); +fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.C) c_int { + const self: *Surface = @ptrCast(@alignCast(ud.?)); self.updateTitleLabels(); self.update_title_timer = null; - return c.FALSE; + return 0; } pub fn getTitle(self: *Surface) ?[:0]const u8 { @@ -1015,22 +1136,22 @@ pub fn setMouseShape( .zoom_out => "zoom-out", }; - const cursor = c.gdk_cursor_new_from_name(name.ptr, null) orelse { + const cursor = gdk.Cursor.newFromName(name.ptr, null) orelse { log.warn("unsupported cursor name={s}", .{name}); return; }; - errdefer c.g_object_unref(cursor); + errdefer cursor.unref(); // Set our new cursor. We only do this if the cursor we currently // have is NOT set to "none" because setting the cursor causes it // to become visible again. - const gl_area_widget: *c.GtkWidget = @ptrCast(@alignCast(self.gl_area)); - if (c.gtk_widget_get_cursor(gl_area_widget) != self.app.cursor_none) { - c.gtk_widget_set_cursor(gl_area_widget, cursor); + const widget = self.gl_area.as(gtk.Widget); + if (widget.getCursor() != self.app.cursor_none) { + widget.setCursor(cursor); } // Free our existing cursor - if (self.cursor) |old| c.g_object_unref(old); + if (self.cursor) |old| old.unref(); self.cursor = cursor; } @@ -1039,22 +1160,22 @@ pub fn setMouseVisibility(self: *Surface, visible: bool) void { // Note in there that self.cursor or cursor_none may be null. That's // not a problem because NULL is a valid argument for set cursor // which means to just use the parent value. - const gl_area_widget: *c.GtkWidget = @ptrCast(@alignCast(self.gl_area)); + const widget = self.gl_area.as(gtk.Widget); if (visible) { - c.gtk_widget_set_cursor(gl_area_widget, self.cursor); + widget.setCursor(self.cursor); return; } + // FIXME: when App is converted to zig-gobject // Set our new cursor to the app "none" cursor - c.gtk_widget_set_cursor(gl_area_widget, self.app.cursor_none); + widget.setCursor(@ptrCast(@alignCast(self.app.cursor_none))); } pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void { const uri = uri_ orelse { if (self.url_widget) |*widget| { - // FIXME: when Surface gets converted to zig-gobject - widget.deinit(@ptrCast(@alignCast(self.overlay))); + widget.deinit(self.overlay); self.url_widget = null; } @@ -1072,8 +1193,7 @@ pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void { return; } - // FIXME: when Surface gets converted to zig-gobject - self.url_widget = URLWidget.init(@ptrCast(@alignCast(self.overlay)), uriZ); + self.url_widget = URLWidget.init(self.overlay, uriZ); } pub fn supportsClipboard( @@ -1102,13 +1222,9 @@ pub fn clipboardRequest( ud_ptr.* = .{ .self = self, .state = state }; // Start our async request - const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); - c.gdk_clipboard_read_text_async( - clipboard, - null, - >kClipboardRead, - ud_ptr, - ); + const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type) orelse return; + + clipboard.readTextAsync(null, gtkClipboardRead, ud_ptr); } pub fn setClipboardString( @@ -1118,8 +1234,9 @@ pub fn setClipboardString( confirm: bool, ) !void { if (!confirm) { - const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); - c.gdk_clipboard_set_text(clipboard, val.ptr); + const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type) orelse return; + clipboard.setText(val); + // We only toast if we are copying to the standard clipboard. if (clipboard_type == .standard and self.app.config.@"app-notifications".@"clipboard-copy") @@ -1147,27 +1264,25 @@ const ClipboardRequest = struct { }; fn gtkClipboardRead( - source: ?*c.GObject, - res: ?*c.GAsyncResult, + source: ?*gobject.Object, + res: *gio.AsyncResult, ud: ?*anyopaque, ) callconv(.C) void { + const clipboard = gobject.ext.cast(gdk.Clipboard, source orelse return) orelse return; const req: *ClipboardRequest = @ptrCast(@alignCast(ud orelse return)); const self = req.self; const alloc = self.app.core_app.alloc; defer alloc.destroy(req); - var gerr: ?*c.GError = null; - const cstr = c.gdk_clipboard_read_text_finish( - @ptrCast(source orelse return), - res, - &gerr, - ); + var gerr: ?*glib.Error = null; + const cstr_ = clipboard.readTextFinish(res, &gerr); if (gerr) |err| { - defer c.g_error_free(err); - log.warn("failed to read clipboard err={s}", .{err.message}); + defer err.free(); + log.warn("failed to read clipboard err={s}", .{err.f_message orelse "(no message)"}); return; } - defer c.g_free(cstr); + const cstr = cstr_ orelse return; + defer glib.free(cstr); const str = std.mem.sliceTo(cstr, 0); self.core_surface.completeClipboardRequest( @@ -1195,10 +1310,10 @@ fn gtkClipboardRead( }; } -fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboard { +fn getClipboard(widget: *gtk.Widget, clipboard: apprt.Clipboard) ?*gdk.Clipboard { return switch (clipboard) { - .standard => c.gtk_widget_get_clipboard(widget), - .selection, .primary => c.gtk_widget_get_primary_clipboard(widget), + .standard => widget.getClipboard(), + .selection, .primary => widget.getPrimaryClipboard(), }; } @@ -1217,35 +1332,33 @@ pub fn showDesktopNotification( else => title, }; - const notification = c.g_notification_new(t.ptr); - defer c.g_object_unref(notification); - c.g_notification_set_body(notification, body.ptr); + const notification = gio.Notification.new(t); + defer notification.unref(); + notification.setBody(body); - const icon = c.g_themed_icon_new(build_config.bundle_id); - defer c.g_object_unref(icon); - c.g_notification_set_icon(notification, icon); + const icon = gio.ThemedIcon.new(build_config.bundle_id); + defer icon.unref(); - const pointer = c.g_variant_new_uint64(@intFromPtr(&self.core_surface)); - c.g_notification_set_default_action_and_target_value( - notification, - "app.present-surface", - pointer, - ); + notification.setIcon(icon); - const g_app: *c.GApplication = @ptrCast(self.app.app); + const pointer = glib.Variant.newUint64(@intFromPtr(&self.core_surface)); + notification.setDefaultActionAndTargetValue("app.present-surface", pointer); + + // FIXME: when App.zig gets converted to zig-gobject + const app: gio.Application = @ptrCast(@alignCast(self.app.app)); // We set the notification ID to the body content. If the content is the // same, this notification may replace a previous notification - c.g_application_send_notification(g_app, body.ptr, notification); + app.sendNotification(body.ptr, notification); } -fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { +fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void { log.debug("gl surface realized", .{}); // We need to make the context current so we can call GL functions. - c.gtk_gl_area_make_current(area); - if (c.gtk_gl_area_get_error(area)) |err| { - log.err("surface failed to realize: {s}", .{err.*.message}); + gl_area.makeCurrent(); + if (gl_area.getError()) |err| { + log.err("surface failed to realize: {s}", .{err.f_message orelse "(no message)"}); log.warn("this error is usually due to a driver or gtk bug", .{}); log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{}); return; @@ -1253,7 +1366,6 @@ fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { // realize means that our OpenGL context is ready, so we can now // initialize the core surface which will setup the renderer. - const self = userdataSelf(ud.?); self.realize() catch |err| { // TODO: we need to destroy the GL area here. log.err("surface failed to realize: {}", .{err}); @@ -1263,28 +1375,21 @@ fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { // When we have a realized surface, we also attach our input method context. // We do this here instead of init because this allows us to release the ref // to the GLArea when we unrealized. - c.gtk_im_context_set_client_widget(self.im_context, @ptrCast(@alignCast(self.overlay))); + self.im_context.as(gtk.IMContext).setClientWidget(self.overlay.as(gtk.Widget)); } /// This is called when the underlying OpenGL resources must be released. /// This is usually due to the OpenGL area changing GDK surfaces. -fn gtkUnrealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { - _ = area; - +fn gtkUnrealize(_: *gtk.GLArea, self: *Surface) callconv(.C) void { log.debug("gl surface unrealized", .{}); - const self = userdataSelf(ud.?); self.core_surface.renderer.displayUnrealized(); // See gtkRealize for why we do this here. - c.gtk_im_context_set_client_widget(self.im_context, null); + self.im_context.as(gtk.IMContext).setClientWidget(null); } /// render signal -fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean { - _ = area; - _ = ctx; - - const self = userdataSelf(ud.?); +fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_int { self.render() catch |err| { log.err("surface failed to render: {}", .{err}); return 0; @@ -1293,21 +1398,22 @@ fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv return 1; } -/// render signal -fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) callconv(.C) void { - const self = userdataSelf(ud.?); - +/// resize signal +fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.C) void { // Some debug output to help understand what GTK is telling us. { const scale_factor = scale: { - const widget = @as(*c.GtkWidget, @ptrCast(area)); - break :scale c.gtk_widget_get_scale_factor(widget); + const widget = gl_area.as(gtk.Widget); + break :scale widget.getScaleFactor(); }; const window_scale_factor = scale: { const window = self.container.window() orelse break :scale 0; - const gdk_surface = c.gtk_native_get_surface(@ptrCast(window.window)); - break :scale c.gdk_surface_get_scale_factor(gdk_surface); + // FIXME: when Window.zig is converted to zig-gobject + const gtk_window: *gtk.Window = @ptrCast(@alignCast(window.window)); + const gtk_native = gtk_window.as(gtk.Native); + const gdk_surface = gtk_native.getSurface() orelse break :scale 0; + break :scale gdk_surface.getScaleFactor(); }; log.debug("gl resize width={} height={} scale={} window_scale={}", .{ @@ -1350,11 +1456,9 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) } /// "destroy" signal for surface -fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { - _ = v; +fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.C) void { log.debug("gl destroy", .{}); - const self = userdataSelf(ud.?); const alloc = self.app.core_app.alloc; self.deinit(); alloc.destroy(self); @@ -1363,14 +1467,15 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { /// Scale x/y by the GDK device scale. fn scaledCoordinates( self: *const Surface, - x: c.gdouble, - y: c.gdouble, + x: f64, + y: f64, ) struct { - x: c.gdouble, - y: c.gdouble, + x: f64, + y: f64, } { + const gl_are_widget = self.gl_area.as(gtk.Widget); const scale_factor: f64 = @floatFromInt( - c.gtk_widget_get_scale_factor(@ptrCast(self.gl_area)), + gl_are_widget.getScaleFactor(), ); return .{ @@ -1380,23 +1485,22 @@ fn scaledCoordinates( } fn gtkMouseDown( - gesture: *c.GtkGestureClick, - _: c.gint, - x: c.gdouble, - y: c.gdouble, - ud: ?*anyopaque, + gesture: *gtk.GestureClick, + _: c_int, + x: f64, + y: f64, + self: *Surface, ) callconv(.C) void { - const event = c.gtk_event_controller_get_current_event(@ptrCast(gesture)) orelse return; + const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return; - const self = userdataSelf(ud.?); - const gtk_mods = c.gdk_event_get_modifier_state(event); + const gtk_mods = event.getModifierState(); - const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture))); + const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); const mods = gtk_key.translateMods(gtk_mods); // If we don't have focus, grab it. - const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); - if (c.gtk_widget_has_focus(gl_widget) == 0) { + const gl_area_widget = self.gl_area.as(gtk.Widget); + if (gl_area_widget.hasFocus() == 0) { self.grabFocus(); } @@ -1414,20 +1518,19 @@ fn gtkMouseDown( } fn gtkMouseUp( - gesture: *c.GtkGestureClick, - _: c.gint, - _: c.gdouble, - _: c.gdouble, - ud: ?*anyopaque, + gesture: *gtk.GestureClick, + _: c_int, + _: f64, + _: f64, + self: *Surface, ) callconv(.C) void { - const event = c.gtk_event_controller_get_current_event(@ptrCast(gesture)) orelse return; + const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return; - const gtk_mods = c.gdk_event_get_modifier_state(event); + const gtk_mods = event.getModifierState(); - const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture))); + const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); const mods = gtk_key.translateMods(gtk_mods); - const self = userdataSelf(ud.?); _ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; @@ -1435,14 +1538,13 @@ fn gtkMouseUp( } fn gtkMouseMotion( - ec: *c.GtkEventControllerMotion, - x: c.gdouble, - y: c.gdouble, - ud: ?*anyopaque, + ec: *gtk.EventControllerMotion, + x: f64, + y: f64, + self: *Surface, ) callconv(.C) void { - const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)) orelse return; + const event = ec.as(gtk.EventController).getCurrentEvent() orelse return; - const self = userdataSelf(ud.?); const scaled = self.scaledCoordinates(x, y); const pos: apprt.CursorPos = .{ @@ -1464,8 +1566,8 @@ fn gtkMouseMotion( if (!is_cursor_still) { // If we don't have focus, and we want it, grab it. - const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); - if (c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") { + const gl_area_widget = self.gl_area.as(gtk.Widget); + if (gl_area_widget.hasFocus() == 0 and self.app.config.@"focus-follows-mouse") { self.grabFocus(); } @@ -1473,7 +1575,7 @@ fn gtkMouseMotion( self.cursor_pos = pos; // Get our modifiers - const gtk_mods = c.gdk_event_get_modifier_state(event); + const gtk_mods = event.getModifierState(); const mods = gtk_key.translateMods(gtk_mods); self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { @@ -1484,15 +1586,13 @@ fn gtkMouseMotion( } fn gtkMouseLeave( - ec: *c.GtkEventControllerMotion, - ud: ?*anyopaque, + ec_motion: *gtk.EventControllerMotion, + self: *Surface, ) callconv(.C) void { - const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)) orelse return; - - const self = userdataSelf(ud.?); + const event = ec_motion.as(gtk.EventController).getCurrentEvent() orelse return; // Get our modifiers - const gtk_mods = c.gdk_event_get_modifier_state(event); + const gtk_mods = event.getModifierState(); const mods = gtk_key.translateMods(gtk_mods); self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| { log.err("error in cursor pos callback err={}", .{err}); @@ -1501,34 +1601,31 @@ fn gtkMouseLeave( } fn gtkMouseScrollPrecisionBegin( - _: *c.GtkEventControllerScroll, - ud: ?*anyopaque, + _: *gtk.EventControllerScroll, + self: *Surface, ) callconv(.C) void { - const self = userdataSelf(ud.?); self.precision_scroll = true; } fn gtkMouseScrollPrecisionEnd( - _: *c.GtkEventControllerScroll, - ud: ?*anyopaque, + _: *gtk.EventControllerScroll, + self: *Surface, ) callconv(.C) void { - const self = userdataSelf(ud.?); self.precision_scroll = false; } fn gtkMouseScroll( - _: *c.GtkEventControllerScroll, - x: c.gdouble, - y: c.gdouble, - ud: ?*anyopaque, -) callconv(.C) void { - const self = userdataSelf(ud.?); + _: *gtk.EventControllerScroll, + x: f64, + y: f64, + self: *Surface, +) callconv(.C) c_int { const scaled = self.scaledCoordinates(x, y); // GTK doesn't support any of the scroll mods. const scroll_mods: input.ScrollMods = .{ .precision = self.precision_scroll }; // Multiply precision scrolls by 10 to get a better response from touchpad scrolling - const multiplier: f64 = if (self.precision_scroll) 10 else 1; + const multiplier: f64 = if (self.precision_scroll) 10.0 else 1.0; self.core_surface.scrollCallback( // We invert because we apply natural scrolling to the values. @@ -1540,42 +1637,42 @@ fn gtkMouseScroll( scroll_mods, ) catch |err| { log.err("error in scroll callback err={}", .{err}); - return; + return 0; }; + + return 1; } fn gtkKeyPressed( - ec_key: *c.GtkEventControllerKey, - keyval: c.guint, - keycode: c.guint, - gtk_mods: c.GdkModifierType, - ud: ?*anyopaque, -) callconv(.C) c.gboolean { - const self = userdataSelf(ud.?); - return if (self.keyEvent( + ec_key: *gtk.EventControllerKey, + keyval: c_uint, + keycode: c_uint, + gtk_mods: gdk.ModifierType, + self: *Surface, +) callconv(.C) c_int { + return @intFromBool(self.keyEvent( .press, ec_key, keyval, keycode, gtk_mods, - )) 1 else 0; + )); } fn gtkKeyReleased( - ec_key: *c.GtkEventControllerKey, - keyval: c.guint, - keycode: c.guint, - state: c.GdkModifierType, - ud: ?*anyopaque, -) callconv(.C) c.gboolean { - const self = userdataSelf(ud.?); - return if (self.keyEvent( + ec_key: *gtk.EventControllerKey, + keyval: c_uint, + keycode: c_uint, + state: gdk.ModifierType, + self: *Surface, +) callconv(.C) void { + _ = self.keyEvent( .release, ec_key, keyval, keycode, state, - )) 1 else 0; + ); } /// Key press event (press or release). @@ -1611,15 +1708,14 @@ fn gtkKeyReleased( pub fn keyEvent( self: *Surface, action: input.Action, - ec_key: *c.GtkEventControllerKey, - keyval: c.guint, - keycode: c.guint, - gtk_mods: c.GdkModifierType, + ec_key: *gtk.EventControllerKey, + keyval: c_uint, + keycode: c_uint, + gtk_mods: gdk.ModifierType, ) bool { // log.warn("GTKIM: keyEvent action={}", .{action}); - const event = c.gtk_event_controller_get_current_event( - @ptrCast(ec_key), - ) orelse return false; + const event = ec_key.as(gtk.EventController).getCurrentEvent() orelse return false; + const key_event = gobject.ext.cast(gdk.KeyEvent, event) orelse return false; // The block below is all related to input method handling. See the function // comment for some high level details and then the comments within @@ -1629,11 +1725,11 @@ pub fn keyEvent( // where the cursor is so it can render the dropdowns in the correct // place. const ime_point = self.core_surface.imePoint(); - c.gtk_im_context_set_cursor_location(self.im_context, &.{ - .x = @intFromFloat(ime_point.x), - .y = @intFromFloat(ime_point.y), - .width = 1, - .height = 1, + self.im_context.as(gtk.IMContext).setCursorLocation(&.{ + .f_x = @intFromFloat(ime_point.x), + .f_y = @intFromFloat(ime_point.y), + .f_width = 1, + .f_height = 1, }); // We note that we're in a keypress because we want some logic to @@ -1669,10 +1765,7 @@ pub fn keyEvent( // 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 = c.gtk_im_context_filter_keypress( - self.im_context, - event, - ) != 0; + const im_handled = self.im_context.as(gtk.IMContext).filterKeypress(event) != 0; // log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{ // im_handled, // self.im_len, @@ -1721,10 +1814,10 @@ pub fn keyEvent( defer self.im_len = 0; // Get the keyvals for this event. - const keyval_unicode = c.gdk_keyval_to_unicode(keyval); + const keyval_unicode = gdk.keyvalToUnicode(keyval); const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted( - @ptrCast(self.gl_area), - event, + self.gl_area.as(gtk.Widget), + key_event, keycode, ); @@ -1745,9 +1838,12 @@ pub fn keyEvent( // Get our consumed modifiers const consumed_mods: input.Mods = consumed: { - const raw = c.gdk_key_event_get_consumed_modifiers(event); - const masked = raw & c.GDK_MODIFIER_MASK; - break :consumed gtk_key.translateMods(masked); + const T = @typeInfo(gdk.ModifierType); + std.debug.assert(T.@"struct".layout == .@"packed"); + const I = T.@"struct".backing_integer.?; + + const masked = @as(I, @bitCast(key_event.getConsumedModifiers())) & @as(I, gdk.MODIFIER_MASK); + break :consumed gtk_key.translateMods(@bitCast(masked)); }; // If we're not in a dead key state, we want to translate our text @@ -1846,7 +1942,7 @@ pub fn keyEvent( // because there is other IME state that we want to preserve, // such as quotation mark ordering for Chinese input. if (self.im_composing) { - c.gtk_im_context_reset(self.im_context); + self.im_context.as(gtk.IMContext).reset(); self.core_surface.preeditCallback(null) catch {}; } @@ -1858,11 +1954,10 @@ pub fn keyEvent( } fn gtkInputPreeditStart( - _: *c.GtkIMContext, - ud: ?*anyopaque, + _: *gtk.IMMulticontext, + self: *Surface, ) callconv(.C) void { // log.warn("GTKIM: preedit start", .{}); - const self = userdataSelf(ud.?); // Start our composing state for the input method and reset our // input buffer to empty. @@ -1871,15 +1966,14 @@ fn gtkInputPreeditStart( } fn gtkInputPreeditChanged( - ctx: *c.GtkIMContext, - ud: ?*anyopaque, + ctx: *gtk.IMMulticontext, + self: *Surface, ) callconv(.C) void { - const self = userdataSelf(ud.?); - // Get our pre-edit string that we'll use to show the user. - var buf: [*c]u8 = undefined; - _ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null); - defer c.g_free(buf); + var buf: [*:0]u8 = undefined; + ctx.as(gtk.IMContext).getPreeditString(&buf, null, null); + defer glib.free(buf); + const str = std.mem.sliceTo(buf, 0); // Update our preedit state in Ghostty core @@ -1890,11 +1984,10 @@ fn gtkInputPreeditChanged( } fn gtkInputPreeditEnd( - _: *c.GtkIMContext, - ud: ?*anyopaque, + _: *gtk.IMMulticontext, + self: *Surface, ) callconv(.C) void { // log.warn("GTKIM: preedit end", .{}); - const self = userdataSelf(ud.?); // End our composing state for GTK, allowing us to commit the text. self.im_composing = false; @@ -1906,11 +1999,10 @@ fn gtkInputPreeditEnd( } fn gtkInputCommit( - _: *c.GtkIMContext, + _: *gtk.IMMulticontext, bytes: [*:0]u8, - ud: ?*anyopaque, + self: *Surface, ) callconv(.C) void { - const self = userdataSelf(ud.?); const str = std.mem.sliceTo(bytes, 0); // log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{ @@ -1985,16 +2077,15 @@ fn gtkInputCommit( }; } -fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { - const self = userdataSelf(ud.?); +fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void { if (!self.realized) return; // Notify our IM context - c.gtk_im_context_focus_in(self.im_context); + self.im_context.as(gtk.IMContext).focusIn(); // Remove the unfocused widget overlay, if we have one if (self.unfocused_widget) |widget| { - c.gtk_overlay_remove_overlay(self.overlay, widget); + self.overlay.removeOverlay(widget); self.unfocused_widget = null; } @@ -2011,12 +2102,11 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo }; } -fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { - const self = userdataSelf(ud.?); +fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void { if (!self.realized) return; // Notify our IM context - c.gtk_im_context_focus_out(self.im_context); + self.im_context.as(gtk.IMContext).focusOut(); // We only try dimming the surface if we are a split switch (self.container) { @@ -2032,8 +2122,8 @@ fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo }; } -/// Adds the unfocused_widget to the overlay. If the unfocused_widget has already been added, this -/// is a no-op +/// Adds the unfocused_widget to the overlay. If the unfocused_widget has +/// already been added, this is a no-op. pub fn dimSurface(self: *Surface) void { _ = self.container.window() orelse { log.warn("dimSurface invalid for container={}", .{self.container}); @@ -2044,17 +2134,23 @@ pub fn dimSurface(self: *Surface) void { // This means we got unfocused due to it opening. if (self.context_menu.isVisible()) return; - if (self.unfocused_widget != null) return; - self.unfocused_widget = c.gtk_drawing_area_new(); - c.gtk_widget_add_css_class(self.unfocused_widget.?, "unfocused-split"); - c.gtk_overlay_add_overlay(self.overlay, self.unfocused_widget.?); + // If there's already an unfocused_widget do nothing; + if (self.unfocused_widget) |_| return; + + self.unfocused_widget = unfocused_widget: { + const drawing_area = gtk.DrawingArea.new(); + const unfocused_widget = drawing_area.as(gtk.Widget); + unfocused_widget.addCssClass("unfocused-split"); + self.overlay.addOverlay(unfocused_widget); + break :unfocused_widget unfocused_widget; + }; } fn userdataSelf(ud: *anyopaque) *Surface { return @ptrCast(@alignCast(ud)); } -fn translateMouseButton(button: c.guint) input.MouseButton { +fn translateMouseButton(button: c_uint) input.MouseButton { return switch (button) { 1 => .left, 2 => .middle, @@ -2077,7 +2173,9 @@ pub fn present(self: *Surface) void { if (window.notebook.getTabPosition(tab)) |position| _ = window.notebook.gotoNthTab(position); } - c.gtk_window_present(window.window); + // FIXME: when Window.zig is converted to zig-gobject + const gtk_window: *gtk.Window = @ptrCast(@alignCast(window.window)); + gtk_window.present(); } self.grabFocus(); @@ -2103,14 +2201,16 @@ pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void { const tab_widget = tab.elem.widget(); const surface_widget = self.primaryWidget(); + // FIXME: when Tab.zig is converted to zig-gobject + const box: *gtk.Box = @ptrCast(@alignCast(tab.box)); if (new_split_zoom) { self.detachFromSplit(); - c.gtk_box_remove(tab.box, tab_widget); - c.gtk_box_append(tab.box, surface_widget); + box.remove(tab_widget); + box.append(surface_widget); } else { - c.gtk_box_remove(tab.box, surface_widget); + box.remove(surface_widget); self.attachToSplit(); - c.gtk_box_append(tab.box, tab_widget); + box.append(tab_widget); } self.zoomed_in = new_split_zoom; @@ -2123,18 +2223,15 @@ pub fn toggleSplitZoom(self: *Surface) void { /// Handle items being dropped on our surface. fn gtkDrop( - _: *c.GtkDropTarget, - value: *c.GValue, - x: f64, - y: f64, - ud: ?*anyopaque, -) callconv(.C) c.gboolean { - _ = x; - _ = y; - const self = userdataSelf(ud.?); + _: *gtk.DropTarget, + value: *gobject.Value, + _: f64, + _: f64, + self: *Surface, +) callconv(.C) c_int { const alloc = self.app.core_app.alloc; - if (g_value_holds(value, c.G_TYPE_BOXED)) { + if (g_value_holds(value, gdk.FileList.getGObjectType())) { var data = std.ArrayList(u8).init(alloc); defer data.deinit(); @@ -2143,12 +2240,13 @@ fn gtkDrop( }; const writer = shell_escape_writer.writer(); - const fl: *c.GdkFileList = @ptrCast(c.g_value_get_boxed(value)); - var l = c.gdk_file_list_get_files(fl); + const unboxed = value.getBoxed() orelse return 0; + const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed)); + var list: ?*glib.SList = fl.getFiles(); - while (l != null) : (l = l.*.next) { - const file: *c.GFile = @ptrCast(l.*.data); - const path = c.g_file_get_path(file) orelse continue; + while (list) |item| : (list = item.f_next) { + const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue)); + const path = file.getPath() orelse continue; writer.writeAll(std.mem.span(path)) catch |err| { log.err("unable to write path to buffer: {}", .{err}); @@ -2162,7 +2260,7 @@ fn gtkDrop( const string = data.toOwnedSliceSentinel(0) catch |err| { log.err("unable to convert to a slice: {}", .{err}); - return 1; + return 0; }; defer alloc.free(string); @@ -2171,9 +2269,41 @@ fn gtkDrop( return 1; } - if (g_value_holds(value, c.G_TYPE_STRING)) { - if (c.g_value_get_string(value)) |string| { - self.doPaste(std.mem.span(string)); + if (g_value_holds(value, gio.File.getGObjectType())) { + const object = value.getObject() orelse return 0; + const file = gobject.ext.cast(gio.File, object) orelse return 0; + const path = file.getPath() orelse return 0; + var data = std.ArrayList(u8).init(alloc); + defer data.deinit(); + + var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ + .child_writer = data.writer(), + }; + const writer = shell_escape_writer.writer(); + writer.writeAll(std.mem.span(path)) catch |err| { + log.err("unable to write path to buffer: {}", .{err}); + return 0; + }; + writer.writeAll("\n") catch |err| { + log.err("unable to write to buffer: {}", .{err}); + return 0; + }; + + const string = data.toOwnedSliceSentinel(0) catch |err| { + log.err("unable to convert to a slice: {}", .{err}); + return 0; + }; + defer alloc.free(string); + + self.doPaste(string); + + return 1; + } + + if (g_value_holds(value, gobject.ext.types.string)) { + if (value.getString()) |string| { + const text = std.mem.span(string); + if (text.len > 0) self.doPaste(text); } return 1; } @@ -2242,10 +2372,10 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { /// Check a GValue to see what's type its wrapping. This is equivalent to GTK's /// `G_VALUE_HOLDS` macro but Zig's C translator does not like it. -fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { +fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool { if (value_) |value| { - if (value.*.g_type == g_type) return true; - return c.g_type_check_value_holds(value, g_type) != 0; + if (value.f_g_type == g_type) return true; + return gobject.typeCheckValueHolds(value, g_type) != 0; } return false; } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 78a6253d7..eecbf4358 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -73,8 +73,9 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { surface.container = .{ .tab_ = self }; self.elem = .{ .surface = surface }; + // FIXME: when Tab.zig is converted to zig-gobject // Add Surface to the Tab - c.gtk_box_append(self.box, surface.primaryWidget()); + c.gtk_box_append(self.box, @ptrCast(@alignCast(surface.primaryWidget()))); // Set the userdata of the box to point to this tab. c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); @@ -103,10 +104,11 @@ pub fn destroy(self: *Tab, alloc: Allocator) void { /// Replace the surface element that this tab is showing. pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { // Remove our previous widget - c.gtk_box_remove(self.box, self.elem.widget()); + // FIXME: when Tab.zig is converted to zig-gobject + c.gtk_box_remove(self.box, @ptrCast(@alignCast(self.elem.widget()))); // Add our new one - c.gtk_box_append(self.box, elem.widget()); + c.gtk_box_append(self.box, @ptrCast(@alignCast(elem.widget()))); self.elem = elem; } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2e2e0c329..62e3a8b86 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -10,6 +10,7 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const gdk = @import("gdk"); const gio = @import("gio"); const glib = @import("glib"); const gobject = @import("gobject"); @@ -297,15 +298,21 @@ pub fn init(self: *Window, app: *App) !void { // We register a key event controller with the window so // we can catch key events when our surface may not be // focused (i.e. when the libadw tab overview is shown). - const ec_key_press = c.gtk_event_controller_key_new(); - errdefer c.g_object_unref(ec_key_press); - c.gtk_widget_add_controller(gtk_widget, ec_key_press); + const ec_key_press = gtk.EventControllerKey.new(); + errdefer ec_key_press.unref(); + c.gtk_widget_add_controller(gtk_widget, @ptrCast(@alignCast(ec_key_press))); // All of our events _ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); + _ = gtk.EventControllerKey.signals.key_pressed.connect( + ec_key_press, + *Window, + gtkKeyPressed, + self, + .{}, + ); // Our actions for the menu initActions(self); @@ -865,14 +872,12 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { } fn gtkKeyPressed( - ec_key: *c.GtkEventControllerKey, - keyval: c.guint, - keycode: c.guint, - gtk_mods: c.GdkModifierType, - ud: ?*anyopaque, + ec_key: *gtk.EventControllerKey, + keyval: c_uint, + keycode: c_uint, + gtk_mods: gdk.ModifierType, + self: *Window, ) callconv(.C) c.gboolean { - const self = userdataSelf(ud.?); - // We only process window-level events currently for the tab // overview. This is primarily defensive programming because // I'm not 100% certain how our logic below will interact with diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 60f12edca..f59d0080d 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -1,5 +1,10 @@ const std = @import("std"); const build_options = @import("build_options"); + +const gdk = @import("gdk"); +const glib = @import("glib"); +const gtk = @import("gtk"); + const input = @import("../../input.zig"); const c = @import("c.zig").c; const winproto = @import("winproto.zig"); @@ -37,63 +42,56 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u return slice[0 .. slice.len - 1 :0]; } -pub fn translateMods(state: c.GdkModifierType) input.Mods { - var mods: input.Mods = .{}; - if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true; - if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true; - if (state & c.GDK_ALT_MASK != 0) mods.alt = true; - if (state & c.GDK_SUPER_MASK != 0) mods.super = true; - - // Lock is dependent on the X settings but we just assume caps lock. - if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; - return mods; +pub fn translateMods(state: gdk.ModifierType) input.Mods { + return .{ + .shift = state.shift_mask, + .ctrl = state.control_mask, + .alt = state.alt_mask, + .super = state.super_mask, + // Lock is dependent on the X settings but we just assume caps lock. + .caps_lock = state.lock_mask, + }; } // Get the unshifted unicode value of the keyval. This is used // by the Kitty keyboard protocol. pub fn keyvalUnicodeUnshifted( - widget: *c.GtkWidget, - event: *c.GdkEvent, - keycode: c.guint, + widget: *gtk.Widget, + event: *gdk.KeyEvent, + keycode: u32, ) u21 { - const display = c.gtk_widget_get_display(widget); + const display = widget.getDisplay(); // We need to get the currently active keyboard layout so we know // what group to look at. - const layout = c.gdk_key_event_get_layout(@ptrCast(event)); + const layout = event.getLayout(); - // Get all the possible keyboard mappings for this keycode. A keycode - // is the physical key pressed. - var keys: [*]c.GdkKeymapKey = undefined; - var keyvals: [*]c.guint = undefined; - var n_keys: c_int = 0; - if (c.gdk_display_map_keycode( - display, - keycode, - @ptrCast(&keys), - @ptrCast(&keyvals), - &n_keys, - ) == 0) return 0; + // Get all the possible keyboard mappings for this keycode. A keycode is the + // physical key pressed. + var keys: [*]gdk.KeymapKey = undefined; + var keyvals: [*]c_uint = undefined; + var n_entries: c_int = 0; + if (display.mapKeycode(keycode, &keys, &keyvals, &n_entries) == 0) return 0; - defer c.g_free(keys); - defer c.g_free(keyvals); + defer glib.free(keys); + defer glib.free(keyvals); // debugging: - // log.debug("layout={}", .{layout}); - // for (0..@intCast(n_keys)) |i| { - // log.debug("keymap key={} codepoint={x}", .{ + // std.log.debug("layout={}", .{layout}); + // for (0..@intCast(n_entries)) |i| { + // std.log.debug("keymap key={} codepoint={x}", .{ // keys[i], - // c.gdk_keyval_to_unicode(keyvals[i]), + // gdk.keyvalToUnicode(keyvals[i]), // }); // } - for (0..@intCast(n_keys)) |i| { - if (keys[i].group == layout and - keys[i].level == 0) + for (0..@intCast(n_entries)) |i| { + if (keys[i].f_group == layout and + keys[i].f_level == 0) { return std.math.cast( u21, - c.gdk_keyval_to_unicode(keyvals[i]), + gdk.keyvalToUnicode(keyvals[i]), ) orelse 0; } } @@ -105,16 +103,16 @@ pub fn keyvalUnicodeUnshifted( /// This requires a lot of context because the GdkEvent /// doesn't contain enough on its own. pub fn eventMods( - event: *c.GdkEvent, + event: *gdk.Event, physical_key: input.Key, - gtk_mods: c.GdkModifierType, + gtk_mods: gdk.ModifierType, action: input.Action, app_winproto: *winproto.App, ) input.Mods { - const device = c.gdk_event_get_device(event); + const device = event.getDevice(); var mods = app_winproto.eventMods(device, gtk_mods); - mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1; + mods.num_lock = if (device) |d| d.getNumLockState() != 0 else false; // We use the physical key to determine sided modifiers. As // far as I can tell there's no other way to reliably determine diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index 7fc7c0bd3..0f36b1622 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -1,6 +1,9 @@ const std = @import("std"); const build_options = @import("build_options"); const Allocator = std.mem.Allocator; + +const gdk = @import("gdk"); + const c = @import("c.zig").c; const Config = @import("../../config.zig").Config; const input = @import("../../input.zig"); @@ -52,8 +55,8 @@ pub const App = union(Protocol) { pub fn eventMods( self: *App, - device: ?*c.GdkDevice, - gtk_mods: c.GdkModifierType, + device: ?*gdk.Device, + gtk_mods: gdk.ModifierType, ) input.Mods { return switch (self.*) { inline else => |*v| v.eventMods(device, gtk_mods), diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index c71394e7a..33ce65bc4 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -1,5 +1,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; + +const gdk = @import("gdk"); + const c = @import("../c.zig").c; const Config = @import("../../../config.zig").Config; const input = @import("../../../input.zig"); @@ -24,8 +27,8 @@ pub const App = struct { pub fn eventMods( _: *App, - _: ?*c.GdkDevice, - _: c.GdkModifierType, + _: ?*gdk.Device, + _: gdk.ModifierType, ) ?input.Mods { return null; } diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 389089ec8..1b64d19d9 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -1,8 +1,8 @@ //! Wayland protocol implementation for the Ghostty GTK apprt. const std = @import("std"); const Allocator = std.mem.Allocator; - const build_options = @import("build_options"); + const wayland = @import("wayland"); const gtk = @import("gtk"); const gtk4_layer_shell = @import("gtk4-layer-shell"); @@ -86,8 +86,8 @@ pub const App = struct { pub fn eventMods( _: *App, - _: ?*c.GdkDevice, - _: c.GdkModifierType, + _: ?*gdk.Device, + _: gdk.ModifierType, ) ?input.Mods { return null; } diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 72054f0eb..89f8d65e4 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -3,6 +3,9 @@ const std = @import("std"); const builtin = @import("builtin"); const build_options = @import("build_options"); const Allocator = std.mem.Allocator; + +const gdk = @import("gdk"); + const c = @import("../c.zig").c; const input = @import("../../../input.zig"); const Config = @import("../../../config.zig").Config; @@ -117,8 +120,8 @@ pub const App = struct { /// event did not result in a modifier change). pub fn eventMods( self: App, - device: ?*c.GdkDevice, - gtk_mods: c.GdkModifierType, + device: ?*gdk.Device, + gtk_mods: gdk.ModifierType, ) ?input.Mods { _ = device; _ = gtk_mods; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index ae9f09afe..0ccf769e8 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -452,12 +452,12 @@ pub fn add( .optimize = optimize, }); const gobject_imports = .{ - .{ "gobject", "gobject2" }, + .{ "adw", "adw1" }, + .{ "gdk", "gdk4" }, .{ "gio", "gio2" }, .{ "glib", "glib2" }, + .{ "gobject", "gobject2" }, .{ "gtk", "gtk4" }, - .{ "gdk", "gdk4" }, - .{ "adw", "adw1" }, }; inline for (gobject_imports) |import| { const name, const module = import;