From ecd14a8739c55465fef3294275d46b29b56c4ff8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 20 Jul 2025 14:00:54 -0700 Subject: [PATCH 1/3] apprt/gtk-ng: surface mouse shape --- src/apprt/gtk-ng/Surface.zig | 6 ++ src/apprt/gtk-ng/class/application.zig | 22 ++++++- src/apprt/gtk-ng/class/surface.zig | 90 ++++++++++++++++++++++++++ src/terminal/mouse_shape.zig | 11 ++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/Surface.zig b/src/apprt/gtk-ng/Surface.zig index dd5d15b3a..ab9b6de52 100644 --- a/src/apprt/gtk-ng/Surface.zig +++ b/src/apprt/gtk-ng/Surface.zig @@ -14,6 +14,12 @@ pub fn deinit(self: *Self) void { _ = self; } +/// Returns the GObject surface for this apprt surface. This is a function +/// so we can add some extra logic if we ever have to here. +pub fn gobj(self: *Self) *Surface { + return self.surface; +} + pub fn core(self: *Self) *CoreSurface { // This asserts the non-optional because libghostty should only // be calling this for initialized surfaces. diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index d65c0df7c..c4ceeb564 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -15,6 +15,7 @@ const cgroup = @import("../cgroup.zig"); const CoreApp = @import("../../../App.zig"); const configpkg = @import("../../../config.zig"); const internal_os = @import("../../../os/main.zig"); +const terminal = @import("../../../terminal/main.zig"); const xev = @import("../../../global.zig").xev; const CoreConfig = configpkg.Config; const CoreSurface = @import("../../../Surface.zig"); @@ -404,6 +405,8 @@ pub const Application = extern struct { value.config, ), + .mouse_shape => Action.mouseShape(target, value), + .new_window => try Action.newWindow( self, switch (target) { @@ -440,7 +443,6 @@ pub const Application = extern struct { .initial_size, .size_limit, .mouse_visibility, - .mouse_shape, .mouse_over_link, .toggle_tab_overview, .toggle_split_zoom, @@ -886,6 +888,24 @@ const Action = struct { } } + pub fn mouseShape( + target: apprt.Target, + shape: terminal.MouseShape, + ) void { + switch (target) { + .app => log.warn("mouse shape to app is unexpected", .{}), + .surface => |surface| { + var value = gobject.ext.Value.newFrom(shape); + defer value.unset(); + gobject.Object.setProperty( + surface.rt_surface.gobj().as(gobject.Object), + "mouse-shape", + &value, + ); + }, + } + } + pub fn newWindow( self: *Application, parent: ?*CoreSurface, diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index c3a2aeef8..8ab180ae6 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -10,6 +10,7 @@ const apprt = @import("../../../apprt.zig"); const input = @import("../../../input.zig"); const internal_os = @import("../../../os/main.zig"); const renderer = @import("../../../renderer.zig"); +const terminal = @import("../../../terminal/main.zig"); const CoreSurface = @import("../../../Surface.zig"); const gresource = @import("../build/gresource.zig"); const adw_version = @import("../adw_version.zig"); @@ -52,6 +53,26 @@ pub const Surface = extern struct { }, ); }; + + pub const @"mouse-shape" = struct { + pub const name = "mouse-shape"; + const impl = gobject.ext.defineProperty( + name, + Self, + terminal.MouseShape, + .{ + .nick = "Mouse Shape", + .blurb = "The current mouse shape to show for the surface.", + .default = .default, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "mouse_shape", + ), + }, + ); + }; }; pub const signals = struct { @@ -81,6 +102,9 @@ pub const Surface = extern struct { /// The configuration that this surface is using. config: ?*Config = null, + /// The mouse shape to show for the surface. + mouse_shape: terminal.MouseShape = .default, + /// 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 = undefined, @@ -687,6 +711,7 @@ pub const Surface = extern struct { 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, @@ -715,6 +740,15 @@ pub const Surface = extern struct { self, .{}, ); + + // Some property signals + _ = gobject.Object.signals.notify.connect( + self, + ?*anyopaque, + &propMouseShape, + null, + .{ .detail = "mouse-shape" }, + ); } fn dispose(self: *Self) callconv(.C) void { @@ -761,6 +795,61 @@ pub const Surface = extern struct { ); } + //--------------------------------------------------------------- + // Properties + + fn propMouseShape( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + const name: [:0]const u8 = switch (priv.mouse_shape) { + .default => "default", + .help => "help", + .pointer => "pointer", + .context_menu => "context-menu", + .progress => "progress", + .wait => "wait", + .cell => "cell", + .crosshair => "crosshair", + .text => "text", + .vertical_text => "vertical-text", + .alias => "alias", + .copy => "copy", + .no_drop => "no-drop", + .move => "move", + .not_allowed => "not-allowed", + .grab => "grab", + .grabbing => "grabbing", + .all_scroll => "all-scroll", + .col_resize => "col-resize", + .row_resize => "row-resize", + .n_resize => "n-resize", + .e_resize => "e-resize", + .s_resize => "s-resize", + .w_resize => "w-resize", + .ne_resize => "ne-resize", + .nw_resize => "nw-resize", + .se_resize => "se-resize", + .sw_resize => "sw-resize", + .ew_resize => "ew-resize", + .ns_resize => "ns-resize", + .nesw_resize => "nesw-resize", + .nwse_resize => "nwse-resize", + .zoom_in => "zoom-in", + .zoom_out => "zoom-out", + }; + + // TODO: mouse visibility + // if (widget.getCursor() != self.app.cursor_none) { + // widget.setCursor(cursor); + // } + + // Set our new cursor. + priv.gl_area.as(gtk.Widget).setCursorFromName(name.ptr); + } + //--------------------------------------------------------------- // Signal Handlers @@ -1371,6 +1460,7 @@ pub const Surface = extern struct { // Properties gobject.ext.registerProperties(class, &.{ properties.config.impl, + properties.@"mouse-shape".impl, }); // Signals diff --git a/src/terminal/mouse_shape.zig b/src/terminal/mouse_shape.zig index aedba2455..e71d4fb3b 100644 --- a/src/terminal/mouse_shape.zig +++ b/src/terminal/mouse_shape.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_config = @import("../build_config.zig"); /// The possible cursor shapes. Not all app runtimes support these shapes. /// The shapes are always based on the W3C supported cursor styles so we @@ -45,6 +46,16 @@ pub const MouseShape = enum(c_int) { pub fn fromString(v: []const u8) ?MouseShape { return string_map.get(v); } + + /// 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( + MouseShape, + .{ .name = "GhosttyMouseShape" }, + ), + + .none => void, + }; }; const string_map = std.StaticStringMap(MouseShape).initComptime(.{ From 4ffbd93ab5d657469baab14d72c7ef189cd51ef9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 20 Jul 2025 14:04:22 -0700 Subject: [PATCH 2/3] apprt/gtk-ng: surface mouse visibility --- src/apprt/gtk-ng/class/application.zig | 23 +++++++++- src/apprt/gtk-ng/class/surface.zig | 59 +++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index c4ceeb564..a170afc74 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -406,6 +406,7 @@ pub const Application = extern struct { ), .mouse_shape => Action.mouseShape(target, value), + .mouse_visibility => Action.mouseVisibility(target, value), .new_window => try Action.newWindow( self, @@ -442,7 +443,6 @@ pub const Application = extern struct { .present_terminal, .initial_size, .size_limit, - .mouse_visibility, .mouse_over_link, .toggle_tab_overview, .toggle_split_zoom, @@ -906,6 +906,27 @@ const Action = struct { } } + pub fn mouseVisibility( + target: apprt.Target, + visibility: apprt.action.MouseVisibility, + ) void { + switch (target) { + .app => log.warn("mouse visibility to app is unexpected", .{}), + .surface => |surface| { + var value = gobject.ext.Value.newFrom(switch (visibility) { + .visible => false, + .hidden => true, + }); + defer value.unset(); + gobject.Object.setProperty( + surface.rt_surface.gobj().as(gobject.Object), + "mouse-hidden", + &value, + ); + }, + } + } + pub fn newWindow( self: *Application, parent: ?*CoreSurface, diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 8ab180ae6..92c86cb1a 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -54,6 +54,26 @@ pub const Surface = extern struct { ); }; + pub const @"mouse-hidden" = struct { + pub const name = "mouse-hidden"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Mouse Hidden", + .blurb = "Whether the mouse cursor should be hidden.", + .default = false, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "mouse_hidden", + ), + }, + ); + }; + pub const @"mouse-shape" = struct { pub const name = "mouse-shape"; const impl = gobject.ext.defineProperty( @@ -105,6 +125,9 @@ pub const Surface = extern struct { /// The mouse shape to show for the surface. mouse_shape: terminal.MouseShape = .default, + /// Whether the mouse should be hidden or not as requested externally. + mouse_hidden: bool = false, + /// 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 = undefined, @@ -742,6 +765,13 @@ pub const Surface = extern struct { ); // Some property signals + _ = gobject.Object.signals.notify.connect( + self, + ?*anyopaque, + &propMouseHidden, + null, + .{ .detail = "mouse-hidden" }, + ); _ = gobject.Object.signals.notify.connect( self, ?*anyopaque, @@ -798,12 +828,35 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Properties + fn propMouseHidden( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + + // If we're hidden we set it to "none" + if (priv.mouse_hidden) { + priv.gl_area.as(gtk.Widget).setCursorFromName("none"); + return; + } + + // If we're not hidden we just trigger the mouse shape + // prop notification to handle setting the proper mouse shape. + self.propMouseShape(undefined, null); + } + fn propMouseShape( self: *Self, _: *gobject.ParamSpec, _: ?*anyopaque, ) callconv(.c) void { const priv = self.private(); + + // If our mouse should be hidden currently then we don't + // do anything. + if (priv.mouse_hidden) return; + const name: [:0]const u8 = switch (priv.mouse_shape) { .default => "default", .help => "help", @@ -841,11 +894,6 @@ pub const Surface = extern struct { .zoom_out => "zoom-out", }; - // TODO: mouse visibility - // if (widget.getCursor() != self.app.cursor_none) { - // widget.setCursor(cursor); - // } - // Set our new cursor. priv.gl_area.as(gtk.Widget).setCursorFromName(name.ptr); } @@ -1461,6 +1509,7 @@ pub const Surface = extern struct { gobject.ext.registerProperties(class, &.{ properties.config.impl, properties.@"mouse-shape".impl, + properties.@"mouse-hidden".impl, }); // Signals From fb2021bc9fdf931ee8696916401bb15a5c07a8bd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 20 Jul 2025 14:38:39 -0700 Subject: [PATCH 3/3] apprt/gtk-ng: correct default mouse shapes --- src/apprt/gtk-ng/class/surface.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 92c86cb1a..0100f61c4 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -83,7 +83,7 @@ pub const Surface = extern struct { .{ .nick = "Mouse Shape", .blurb = "The current mouse shape to show for the surface.", - .default = .default, + .default = .text, .accessor = gobject.ext.privateFieldAccessor( Self, Private, @@ -563,6 +563,8 @@ pub const Surface = extern struct { priv.rt_surface = .{ .surface = self }; priv.precision_scroll = false; priv.cursor_pos = .{ .x = 0, .y = 0 }; + priv.mouse_shape = .text; + priv.mouse_hidden = false; priv.size = .{ // Funky numbers on purpose so they stand out if for some reason // our size doesn't get properly set.