apprt/gtk-ng: mouse shape,visibility (#7999)

Relatively simple port. A few cool things:

1. We use properties on `GhosttySurface` to set this now and standard
property listeners
2. We make `terminal.MouseShape` a GObject enum if we have gobject
available.
3. The property based approach means we don't have to manage
`*gdk.Cursor` memory anywhere anymore.

And, we're still Valgrind clean.
This commit is contained in:
Mitchell Hashimoto
2025-07-20 14:53:24 -07:00
committed by GitHub
4 changed files with 201 additions and 2 deletions

View File

@ -14,6 +14,12 @@ pub fn deinit(self: *Self) void {
_ = self; _ = 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 { pub fn core(self: *Self) *CoreSurface {
// This asserts the non-optional because libghostty should only // This asserts the non-optional because libghostty should only
// be calling this for initialized surfaces. // be calling this for initialized surfaces.

View File

@ -15,6 +15,7 @@ const cgroup = @import("../cgroup.zig");
const CoreApp = @import("../../../App.zig"); const CoreApp = @import("../../../App.zig");
const configpkg = @import("../../../config.zig"); const configpkg = @import("../../../config.zig");
const internal_os = @import("../../../os/main.zig"); const internal_os = @import("../../../os/main.zig");
const terminal = @import("../../../terminal/main.zig");
const xev = @import("../../../global.zig").xev; const xev = @import("../../../global.zig").xev;
const CoreConfig = configpkg.Config; const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig"); const CoreSurface = @import("../../../Surface.zig");
@ -404,6 +405,9 @@ pub const Application = extern struct {
value.config, value.config,
), ),
.mouse_shape => Action.mouseShape(target, value),
.mouse_visibility => Action.mouseVisibility(target, value),
.new_window => try Action.newWindow( .new_window => try Action.newWindow(
self, self,
switch (target) { switch (target) {
@ -439,8 +443,6 @@ pub const Application = extern struct {
.present_terminal, .present_terminal,
.initial_size, .initial_size,
.size_limit, .size_limit,
.mouse_visibility,
.mouse_shape,
.mouse_over_link, .mouse_over_link,
.toggle_tab_overview, .toggle_tab_overview,
.toggle_split_zoom, .toggle_split_zoom,
@ -886,6 +888,45 @@ 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 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( pub fn newWindow(
self: *Application, self: *Application,
parent: ?*CoreSurface, parent: ?*CoreSurface,

View File

@ -10,6 +10,7 @@ const apprt = @import("../../../apprt.zig");
const input = @import("../../../input.zig"); const input = @import("../../../input.zig");
const internal_os = @import("../../../os/main.zig"); const internal_os = @import("../../../os/main.zig");
const renderer = @import("../../../renderer.zig"); const renderer = @import("../../../renderer.zig");
const terminal = @import("../../../terminal/main.zig");
const CoreSurface = @import("../../../Surface.zig"); const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig"); const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig"); const adw_version = @import("../adw_version.zig");
@ -52,6 +53,46 @@ 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(
name,
Self,
terminal.MouseShape,
.{
.nick = "Mouse Shape",
.blurb = "The current mouse shape to show for the surface.",
.default = .text,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"mouse_shape",
),
},
);
};
}; };
pub const signals = struct { pub const signals = struct {
@ -81,6 +122,12 @@ pub const Surface = extern struct {
/// The configuration that this surface is using. /// The configuration that this surface is using.
config: ?*Config = null, config: ?*Config = null,
/// 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 /// The GLAarea that renders the actual surface. This is a binding
/// to the template so it doesn't have to be unrefed manually. /// to the template so it doesn't have to be unrefed manually.
gl_area: *gtk.GLArea = undefined, gl_area: *gtk.GLArea = undefined,
@ -516,6 +563,8 @@ pub const Surface = extern struct {
priv.rt_surface = .{ .surface = self }; priv.rt_surface = .{ .surface = self };
priv.precision_scroll = false; priv.precision_scroll = false;
priv.cursor_pos = .{ .x = 0, .y = 0 }; priv.cursor_pos = .{ .x = 0, .y = 0 };
priv.mouse_shape = .text;
priv.mouse_hidden = false;
priv.size = .{ priv.size = .{
// Funky numbers on purpose so they stand out if for some reason // Funky numbers on purpose so they stand out if for some reason
// our size doesn't get properly set. // our size doesn't get properly set.
@ -687,6 +736,7 @@ pub const Surface = extern struct {
gl_area.setHasStencilBuffer(0); gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(0); gl_area.setHasDepthBuffer(0);
gl_area.setUseEs(0); gl_area.setUseEs(0);
gl_area.as(gtk.Widget).setCursorFromName("text");
_ = gtk.Widget.signals.realize.connect( _ = gtk.Widget.signals.realize.connect(
gl_area, gl_area,
*Self, *Self,
@ -715,6 +765,22 @@ pub const Surface = extern struct {
self, self,
.{}, .{},
); );
// Some property signals
_ = gobject.Object.signals.notify.connect(
self,
?*anyopaque,
&propMouseHidden,
null,
.{ .detail = "mouse-hidden" },
);
_ = gobject.Object.signals.notify.connect(
self,
?*anyopaque,
&propMouseShape,
null,
.{ .detail = "mouse-shape" },
);
} }
fn dispose(self: *Self) callconv(.C) void { fn dispose(self: *Self) callconv(.C) void {
@ -761,6 +827,79 @@ 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",
.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",
};
// Set our new cursor.
priv.gl_area.as(gtk.Widget).setCursorFromName(name.ptr);
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Signal Handlers // Signal Handlers
@ -1371,6 +1510,8 @@ pub const Surface = extern struct {
// Properties // Properties
gobject.ext.registerProperties(class, &.{ gobject.ext.registerProperties(class, &.{
properties.config.impl, properties.config.impl,
properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl,
}); });
// Signals // Signals

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const build_config = @import("../build_config.zig");
/// The possible cursor shapes. Not all app runtimes support these shapes. /// The possible cursor shapes. Not all app runtimes support these shapes.
/// The shapes are always based on the W3C supported cursor styles so we /// 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 { pub fn fromString(v: []const u8) ?MouseShape {
return string_map.get(v); 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(.{ const string_map = std.StaticStringMap(MouseShape).initComptime(.{