apprt/gtk-ng: present surface

This commit is contained in:
Mitchell Hashimoto
2025-07-30 09:19:15 -07:00
parent 60b859dbf2
commit e7ea084cc3
3 changed files with 135 additions and 1 deletions

View File

@@ -521,6 +521,8 @@ pub const Application = extern struct {
.pwd => Action.pwd(target, value),
.present_terminal => return Action.presentTerminal(target),
.progress_report => return Action.progressReport(target, value),
.quit => self.quit(),
@@ -550,7 +552,6 @@ pub const Application = extern struct {
.goto_split,
.inspector,
.desktop_notification,
.present_terminal,
.initial_size,
.size_limit,
.toggle_split_zoom,
@@ -828,6 +829,7 @@ pub const Application = extern struct {
.{ "new-window", actionNewWindow, null },
.{ "new-window-command", actionNewWindow, as_variant_type },
.{ "open-config", actionOpenConfig, null },
.{ "present-surface", actionPresentSurface, t_variant_type },
.{ "quit", actionQuit, null },
.{ "reload-config", actionReloadConfig, null },
};
@@ -1156,6 +1158,50 @@ pub const Application = extern struct {
_ = self.core().mailbox.push(.open_config, .forever);
}
fn actionPresentSurface(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const parameter = parameter_ orelse return;
const t = glib.ext.VariantType.newFor(u64);
defer glib.VariantType.free(t);
// Make sure that we've receiived a u64 from the system.
if (glib.Variant.isOfType(parameter, t) == 0) {
return;
}
// Convert that u64 to pointer to a core surface. A value of zero
// means that there was no target surface for the notification so
// we don't focus any surface.
//
// This is admittedly SUPER SUS and we should instead do what we
// do on macOS which is generate a UUID per surface and then pass
// that around. But, we do validate the pointer below so at worst
// this may result in focusing the wrong surface if the pointer was
// reused for a surface.
const ptr_int = parameter.getUint64();
if (ptr_int == 0) return;
const surface: *CoreSurface = @ptrFromInt(ptr_int);
// Send a message through the core app mailbox rather than presenting the
// surface directly so that it can validate that the surface pointer is
// valid. We could get an invalid pointer if a desktop notification outlives
// a Ghostty instance and a new one starts up, or there are multiple Ghostty
// instances running.
_ = self.core().mailbox.push(
.{
.surface_message = .{
.surface = surface,
.message = .present_surface,
},
},
.forever,
);
}
//----------------------------------------------------------------
// Boilerplate/Noise
@@ -1445,6 +1491,18 @@ const Action = struct {
}
}
pub fn presentTerminal(
target: apprt.Target,
) bool {
return switch (target) {
.app => false,
.surface => |v| surface: {
v.rt_surface.surface.present();
break :surface true;
},
};
}
pub fn progressReport(
target: apprt.Target,
value: terminal.osc.Command.ProgressReport,

View File

@@ -285,6 +285,19 @@ pub const Surface = extern struct {
);
};
/// Emitted when the focus wants to be brought to the top and
/// focused.
pub const @"present-request" = struct {
pub const name = "present-request";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when this surface requests its container to toggle its
/// fullscreen state.
pub const @"toggle-fullscreen" = struct {
@@ -578,6 +591,17 @@ pub const Surface = extern struct {
return @intFromBool(glib.SOURCE_REMOVE);
}
/// Request that this terminal come to the front and become focused.
/// It is up to the embedding widget to react to this.
pub fn present(self: *Self) void {
signals.@"present-request".impl.emit(
self,
null,
.{},
null,
);
}
/// Key press event (press or release).
///
/// At a high level, we want to construct an `input.KeyEvent` and
@@ -2173,6 +2197,7 @@ pub const Surface = extern struct {
signals.bell.impl.register(.{});
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
signals.@"present-request".impl.register(.{});
signals.@"toggle-fullscreen".impl.register(.{});
signals.@"toggle-maximize".impl.register(.{});

View File

@@ -955,6 +955,13 @@ pub const Window = extern struct {
self,
.{},
);
_ = Surface.signals.@"present-request".connect(
surface,
*Self,
surfacePresentRequest,
self,
.{},
);
_ = Surface.signals.@"clipboard-write".connect(
surface,
*Self,
@@ -1093,6 +1100,50 @@ pub const Window = extern struct {
}
}
fn surfacePresentRequest(
surface: *Surface,
self: *Self,
) callconv(.c) void {
// Verify that this surface is actually in this window.
{
const surface_window = ext.getAncestor(
Self,
surface.as(gtk.Widget),
) orelse {
log.warn(
"present request called for non-existent surface",
.{},
);
return;
};
if (surface_window != self) {
log.warn(
"present request called for surface in different window",
.{},
);
return;
}
}
// Get the tab for this surface.
const tab = ext.getAncestor(
Tab,
surface.as(gtk.Widget),
) orelse {
log.warn("present request surface not found", .{});
return;
};
// Get the page that contains this tab
const priv = self.private();
const tab_view = priv.tab_view;
const page = tab_view.getPage(tab.as(gtk.Widget));
tab_view.setSelectedPage(page);
// Grab focus
surface.grabFocus();
}
fn surfaceToggleFullscreen(
surface: *Surface,
self: *Self,