apprt/gtk-ng: surface context menu

pull/8144/head
Mitchell Hashimoto 2025-08-04 11:20:38 -07:00
parent 1d62f37cbb
commit ee6d9b3116
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 210 additions and 37 deletions

View File

@ -327,6 +327,18 @@ pub const Surface = extern struct {
);
};
/// Emitted just prior to the context menu appearing.
pub const menu = struct {
pub const name = "menu";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when the focus wants to be brought to the top and
/// focused.
pub const @"present-request" = struct {
@ -462,6 +474,7 @@ pub const Surface = extern struct {
// Template binds
child_exited_overlay: *ChildExited,
context_menu: *gtk.PopoverMenu,
drop_target: *gtk.DropTarget,
progress_bar_overlay: *gtk.ProgressBar,
@ -1473,6 +1486,16 @@ pub const Surface = extern struct {
self.close(.{ .surface = false });
}
fn contextMenuClosed(
_: *gtk.PopoverMenu,
self: *Self,
) callconv(.c) void {
// When the context menu closes, it moves focus back to the tab
// bar if there are tabs. That's not correct. We need to grab it
// on the surface.
self.grabFocus();
}
fn dtDrop(
_: *gtk.DropTarget,
value: *gobject.Value,
@ -1647,9 +1670,9 @@ pub const Surface = extern struct {
}
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const consumed = if (priv.core_surface) |surface| consumed: {
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const mods = gtk_key.translateMods(gtk_mods);
break :consumed surface.mouseButtonCallback(
.press,
@ -1661,10 +1684,28 @@ pub const Surface = extern struct {
};
} else false;
// TODO: context menu
_ = consumed;
_ = x;
_ = y;
// If a right click isn't consumed, mouseButtonCallback selects the hovered
// word and returns false. We can use this to handle the context menu
// opening under normal scenarios.
if (!consumed and button == .right) {
signals.menu.impl.emit(
self,
null,
.{},
null,
);
const rect: gdk.Rectangle = .{
.f_x = @intFromFloat(x),
.f_y = @intFromFloat(y),
.f_width = 1,
.f_height = 1,
};
const popover = priv.context_menu.as(gtk.Popover);
popover.setPointingTo(&rect);
popover.popup();
}
}
fn gcMouseUp(
@ -2259,6 +2300,7 @@ pub const Surface = extern struct {
class.bindTemplateChildPrivate("url_left", .{});
class.bindTemplateChildPrivate("url_right", .{});
class.bindTemplateChildPrivate("child_exited_overlay", .{});
class.bindTemplateChildPrivate("context_menu", .{});
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
class.bindTemplateChildPrivate("resize_overlay", .{});
class.bindTemplateChildPrivate("drop_target", .{});
@ -2288,6 +2330,7 @@ pub const Surface = extern struct {
class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter);
class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave);
class.bindTemplateCallback("child_exited_close", &childExitedClose);
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
@ -2315,6 +2358,7 @@ pub const Surface = extern struct {
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});
signals.menu.impl.register(.{});
signals.@"present-request".impl.register(.{});
signals.@"toggle-fullscreen".impl.register(.{});
signals.@"toggle-maximize".impl.register(.{});

View File

@ -590,6 +590,27 @@ pub const Window = extern struct {
};
}
/// Sync the state of any actions on this window.
fn syncActions(self: *Self) void {
const has_selection = selection: {
const surface = self.getActiveSurface() orelse
break :selection false;
const core_surface = surface.core() orelse
break :selection false;
break :selection core_surface.hasSelection();
};
const action_map: *gio.ActionMap = gobject.ext.cast(
gio.ActionMap,
self,
) orelse return;
const action: *gio.SimpleAction = gobject.ext.cast(
gio.SimpleAction,
action_map.lookupAction("copy") orelse return,
) orelse return;
action.setEnabled(@intFromBool(has_selection));
}
fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
const widget = self.as(gtk.Widget);
if (value)
@ -845,23 +866,7 @@ pub const Window = extern struct {
const active = button.getActive() != 0;
if (!active) return;
const has_selection = selection: {
const surface = self.getActiveSurface() orelse
break :selection false;
const core_surface = surface.core() orelse
break :selection false;
break :selection core_surface.hasSelection();
};
const action_map: *gio.ActionMap = gobject.ext.cast(
gio.ActionMap,
self,
) orelse return;
const action: *gio.SimpleAction = gobject.ext.cast(
gio.SimpleAction,
action_map.lookupAction("copy") orelse return,
) orelse return;
action.setEnabled(@intFromBool(has_selection));
self.syncActions();
}
fn propQuickTerminal(
@ -1210,6 +1215,13 @@ pub const Window = extern struct {
self,
.{},
);
_ = Surface.signals.menu.connect(
surface,
*Self,
surfaceMenu,
self,
.{},
);
_ = Surface.signals.@"toggle-fullscreen".connect(
surface,
*Self,
@ -1355,6 +1367,13 @@ pub const Window = extern struct {
}
}
fn surfaceMenu(
_: *Surface,
self: *Self,
) callconv(.c) void {
self.syncActions();
}
fn surfacePresentRequest(
surface: *Surface,
self: *Self,

View File

@ -15,19 +15,32 @@ template $GhosttySurface: Adw.Bin {
focusable: false;
focus-on-click: false;
GLArea gl_area {
realize => $gl_realize();
unrealize => $gl_unrealize();
render => $gl_render();
resize => $gl_resize();
child: Box {
hexpand: true;
vexpand: true;
focusable: true;
focus-on-click: true;
has-stencil-buffer: false;
has-depth-buffer: false;
use-es: 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;
}
PopoverMenu context_menu {
closed => $context_menu_closed();
menu-model: context_menu_model;
flags: nested;
halign: start;
has-arrow: false;
}
};
[overlay]
ProgressBar progress_bar_overlay {
@ -122,3 +135,104 @@ IMMulticontext im_context {
preedit-end => $im_preedit_end();
commit => $im_commit();
}
menu context_menu_model {
section {
item {
label: _("Copy");
action: "win.copy";
}
item {
label: _("Paste");
action: "win.paste";
}
}
section {
item {
label: _("Clear");
action: "win.clear";
}
item {
label: _("Reset");
action: "win.reset";
}
}
section {
submenu {
label: _("Split");
item {
label: _("Change Title…");
action: "win.prompt-title";
}
item {
label: _("Split Up");
action: "win.split-up";
}
item {
label: _("Split Down");
action: "win.split-down";
}
item {
label: _("Split Left");
action: "win.split-left";
}
item {
label: _("Split Right");
action: "win.split-right";
}
}
submenu {
label: _("Tab");
item {
label: _("New Tab");
action: "win.new-tab";
}
item {
label: _("Close Tab");
action: "win.close-tab";
}
}
submenu {
label: _("Window");
item {
label: _("New Window");
action: "win.new-window";
}
item {
label: _("Close Window");
action: "win.close";
}
}
}
section {
submenu {
label: _("Config");
item {
label: _("Open Configuration");
action: "app.open-config";
}
item {
label: _("Reload Configuration");
action: "app.reload-config";
}
}
}
}

View File

@ -82,10 +82,6 @@ pub fn Menu(
return self.menu_widget.as(gtk.Widget).getVisible() != 0;
}
pub fn setVisible(self: *const Self, visible: bool) void {
self.menu_widget.as(gtk.Widget).setVisible(@intFromBool(visible));
}
/// Refresh the menu. Right now that means enabling/disabling the "Copy"
/// menu item based on whether there is an active selection or not, but
/// that may change in the future.