diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 96275684e5..6b434940e4 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -58,12 +58,6 @@ single_instance: bool,
/// The "none" cursor. We use one that is shared across the entire app.
cursor_none: ?*c.GdkCursor,
-/// The shared application menu.
-menu: ?*c.GMenu = null,
-
-/// The shared context menu.
-context_menu: ?*c.GMenu = null,
-
/// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null,
@@ -480,8 +474,6 @@ pub fn terminate(self: *App) void {
c.g_object_unref(self.app);
if (self.cursor_none) |cursor| c.g_object_unref(cursor);
- if (self.menu) |menu| c.g_object_unref(menu);
- if (self.context_menu) |context_menu| c.g_object_unref(context_menu);
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
for (self.custom_css_providers.items) |provider| {
@@ -1030,20 +1022,28 @@ fn updateConfigErrors(self: *App) !void {
}
fn syncActionAccelerators(self: *App) !void {
- try self.syncActionAccelerator("app.quit", .{ .quit = {} });
- try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
- try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
- try self.syncActionAccelerator("win.toggle_inspector", .{ .inspector = .toggle });
- try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
- try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
- try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
- try self.syncActionAccelerator("win.split_right", .{ .new_split = .right });
- try self.syncActionAccelerator("win.split_down", .{ .new_split = .down });
- try self.syncActionAccelerator("win.split_left", .{ .new_split = .left });
- try self.syncActionAccelerator("win.split_up", .{ .new_split = .up });
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
+
+ try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
+ try self.syncActionAccelerator("win.close", .{ .close_window = {} });
+
+ try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
+ try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
+
+ try self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
+ try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
+ try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
+ try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
+
+ try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
+
+ try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
+ try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
+ try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
+
+ try self.syncActionAccelerator("app.quit", .{ .quit = {} });
}
fn syncActionAccelerator(
@@ -1274,10 +1274,8 @@ pub fn run(self: *App) !void {
// Setup our D-Bus connection for listening to settings changes.
self.initDbus();
- // Setup our menu items
+ // Setup our actions
self.initActions();
- self.initMenu();
- self.initContextMenu();
// Setup our initial color scheme
self.colorSchemeEvent(self.getColorScheme());
@@ -1817,89 +1815,6 @@ fn initActions(self: *App) void {
}
}
-/// Initializes and populates the provided GMenu with sections and actions.
-/// This function is used to set up the application's menu structure, either for
-/// the main menu button or as a context menu when window decorations are disabled.
-fn initMenuContent(menu: *c.GMenu) void {
- {
- const section = c.g_menu_new();
- defer c.g_object_unref(section);
- c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
- c.g_menu_append(section, "New Window", "win.new_window");
- c.g_menu_append(section, "New Tab", "win.new_tab");
- c.g_menu_append(section, "Close Tab", "win.close_tab");
- c.g_menu_append(section, "Split Right", "win.split_right");
- c.g_menu_append(section, "Split Down", "win.split_down");
- c.g_menu_append(section, "Close Window", "win.close");
- }
-
- {
- const section = c.g_menu_new();
- defer c.g_object_unref(section);
- c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
- c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
- c.g_menu_append(section, "Open Configuration", "app.open-config");
- c.g_menu_append(section, "Reload Configuration", "app.reload-config");
- c.g_menu_append(section, "About Ghostty", "win.about");
- }
-}
-
-/// This sets the self.menu property to the application menu that can be
-/// shared by all application windows.
-fn initMenu(self: *App) void {
- const menu = c.g_menu_new();
- errdefer c.g_object_unref(menu);
- initMenuContent(@ptrCast(menu));
- self.menu = menu;
-}
-
-fn initContextMenu(self: *App) void {
- const menu = c.g_menu_new();
- errdefer c.g_object_unref(menu);
-
- {
- const section = c.g_menu_new();
- defer c.g_object_unref(section);
- c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
- c.g_menu_append(section, "Copy", "win.copy");
- c.g_menu_append(section, "Paste", "win.paste");
- }
-
- {
- const section = c.g_menu_new();
- defer c.g_object_unref(section);
- c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
- c.g_menu_append(section, "Split Right", "win.split_right");
- c.g_menu_append(section, "Split Down", "win.split_down");
- }
-
- {
- const section = c.g_menu_new();
- defer c.g_object_unref(section);
- c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
- c.g_menu_append(section, "Reset", "win.reset");
- c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
- }
-
- if (!self.config.@"window-decoration".isCSD()) {
- const section = c.g_menu_new();
- defer c.g_object_unref(section);
- const submenu = c.g_menu_new();
- defer c.g_object_unref(submenu);
-
- initMenuContent(@ptrCast(submenu));
- c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
- c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
- }
-
- self.context_menu = menu;
-}
-
-pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
- const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
- c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
-}
-
fn isValidAppId(app_id: [:0]const u8) bool {
if (app_id.len > 255 or app_id.len == 0) return false;
if (app_id[0] == '.') return false;
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 61866dcec6..51c09bfc5e 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -20,6 +20,7 @@ const App = @import("App.zig");
const Split = @import("Split.zig");
const Tab = @import("Tab.zig");
const Window = @import("Window.zig");
+const Menu = @import("menu.zig").Menu;
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const ResizeOverlay = @import("ResizeOverlay.zig");
const inspector = @import("inspector.zig");
@@ -379,6 +380,9 @@ im_len: u7 = 0,
/// details on what this is.
cgroup_path: ?[]const u8 = null,
+/// Our context menu.
+menu: Menu(Surface),
+
/// Configuration used for initializing the surface. We have to copy some
/// data since initialization is delayed with GTK (on realize).
pub const InitConfig = struct {
@@ -563,9 +567,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.cursor_pos = .{ .x = 0, .y = 0 },
.im_context = im_context,
.cgroup_path = cgroup_path,
+ .menu = undefined,
};
errdefer self.* = undefined;
+ // initialize the context menu
+ self.menu.init();
+ self.menu.setParent(overlay);
+
// Set our default mouse shape
try self.setMouseShape(.text);
@@ -1214,6 +1223,7 @@ fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboa
.selection, .primary => c.gtk_widget_get_primary_clipboard(widget),
};
}
+
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
return self.cursor_pos;
}
@@ -1251,38 +1261,6 @@ pub fn showDesktopNotification(
c.g_application_send_notification(g_app, body.ptr, notification);
}
-fn showContextMenu(self: *Surface, x: f32, y: f32) void {
- const window: *Window = self.container.window() orelse {
- log.info(
- "showContextMenu invalid for container={s}",
- .{@tagName(self.container)},
- );
- return;
- };
-
- var point: c.graphene_point_t = .{ .x = x, .y = y };
- if (c.gtk_widget_compute_point(
- self.primaryWidget(),
- @ptrCast(window.window),
- &c.GRAPHENE_POINT_INIT(point.x, point.y),
- @ptrCast(&point),
- ) == 0) {
- log.warn("failed computing point for context menu", .{});
- return;
- }
-
- const rect: c.GdkRectangle = .{
- .x = @intFromFloat(point.x),
- .y = @intFromFloat(point.y),
- .width = 1,
- .height = 1,
- };
-
- c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect);
- self.app.refreshContextMenu(window.window, self.core_surface.hasSelection());
- c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
-}
-
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
log.debug("gl surface realized", .{});
@@ -1453,7 +1431,7 @@ fn gtkMouseDown(
// word and returns false. We can use this to handle the context menu
// opening under normal scenarios.
if (!consumed and button == .right) {
- self.showContextMenu(@floatCast(x), @floatCast(y));
+ self.menu.popupAt(x, y);
}
}
@@ -1999,15 +1977,14 @@ 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
pub fn dimSurface(self: *Surface) void {
- const window = self.container.window() orelse {
+ _ = self.container.window() orelse {
log.warn("dimSurface invalid for container={}", .{self.container});
return;
};
// Don't dim surface if context menu is open.
// This means we got unfocused due to it opening.
- const context_menu_open = c.gtk_widget_get_visible(window.context_menu);
- if (context_menu_open == 1) return;
+ if (self.menu.isVisible()) return;
if (self.unfocused_widget != null) return;
self.unfocused_widget = c.gtk_drawing_area_new();
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 10af251010..a89381cc53 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -18,6 +18,7 @@ const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const Color = configpkg.Config.Color;
const Surface = @import("Surface.zig");
+const Menu = @import("menu.zig").Menu;
const Tab = @import("Tab.zig");
const c = @import("c.zig").c;
const adwaita = @import("adwaita.zig");
@@ -47,7 +48,8 @@ tab_overview: ?*c.GtkWidget,
/// can be either c.GtkNotebook or c.AdwTabView.
notebook: Notebook,
-context_menu: *c.GtkWidget,
+/// The "main" menu that is attached to a button in the headerbar.
+menu: Menu(Window),
/// The libadwaita widget for receiving toast send requests. If libadwaita is
/// not used, this is null and unused.
@@ -81,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void {
.headerbar = undefined,
.tab_overview = null,
.notebook = undefined,
- .context_menu = undefined,
+ .menu = undefined,
.toast_overlay = undefined,
.winproto = .none,
};
@@ -123,6 +125,9 @@ pub fn init(self: *Window, app: *App) !void {
// Create our box which will hold our widgets in the main content area.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
+ // Set up the menu
+ self.menu.init();
+
// Setup our notebook
self.notebook.init();
@@ -160,7 +165,15 @@ pub fn init(self: *Window, app: *App) !void {
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
- c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
+ c.gtk_menu_button_set_popover(@ptrCast(btn), self.menu.asWidget());
+ _ = c.g_signal_connect_data(
+ btn,
+ "notify::active",
+ c.G_CALLBACK(>kMenuActivate),
+ self,
+ null,
+ c.G_CONNECT_DEFAULT,
+ );
self.headerbar.packEnd(btn);
}
@@ -257,11 +270,6 @@ pub fn init(self: *Window, app: *App) !void {
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
}
- self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
- c.gtk_widget_set_parent(self.context_menu, box);
- c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0);
- c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START);
-
// If we want the window to be maximized, we do that here.
if (app.config.maximize) c.gtk_window_maximize(self.window);
@@ -276,7 +284,6 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_widget_add_controller(window, ec_key_press);
// All of our events
- _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
@@ -445,16 +452,18 @@ fn initActions(self: *Window) void {
const actions = .{
.{ "about", >kActionAbout },
.{ "close", >kActionClose },
- .{ "new_window", >kActionNewWindow },
- .{ "new_tab", >kActionNewTab },
- .{ "split_right", >kActionSplitRight },
- .{ "split_down", >kActionSplitDown },
- .{ "split_left", >kActionSplitLeft },
- .{ "split_up", >kActionSplitUp },
- .{ "toggle_inspector", >kActionToggleInspector },
+ .{ "new-window", >kActionNewWindow },
+ .{ "new-tab", >kActionNewTab },
+ .{ "close-tab", >kActionCloseTab },
+ .{ "split-right", >kActionSplitRight },
+ .{ "split-down", >kActionSplitDown },
+ .{ "split-left", >kActionSplitLeft },
+ .{ "split-up", >kActionSplitUp },
+ .{ "toggle-inspector", >kActionToggleInspector },
.{ "copy", >kActionCopy },
.{ "paste", >kActionPaste },
.{ "reset", >kActionReset },
+ .{ "clear", >kActionClear },
};
inline for (actions) |entry| {
@@ -473,8 +482,6 @@ fn initActions(self: *Window) void {
}
pub fn deinit(self: *Window) void {
- c.gtk_widget_unparent(@ptrCast(self.context_menu));
-
self.winproto.deinit(self.app.core_app.alloc);
if (self.adw_tab_overview_focus_timer) |timer| {
@@ -742,16 +749,6 @@ fn adwTabOverviewFocusTimer(
return 0;
}
-fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
- _ = v;
- log.debug("refocus term request", .{});
- const self = userdataSelf(ud.?);
-
- self.focusCurrentTab();
-
- return true;
-}
-
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
_ = v;
log.debug("window close request", .{});
@@ -912,11 +909,7 @@ fn gtkActionClose(
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud orelse return));
- const surface = self.actionSurface() orelse return;
- _ = surface.performBindingAction(.{ .close_surface = {} }) catch |err| {
- log.warn("error performing binding action error={}", .{err});
- return;
- };
+ c.gtk_window_destroy(self.window);
}
fn gtkActionNewWindow(
@@ -941,6 +934,19 @@ fn gtkActionNewTab(
gtkTabNewClick(undefined, ud);
}
+fn gtkActionCloseTab(
+ _: *c.GSimpleAction,
+ _: *c.GVariant,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const self: *Window = @ptrCast(@alignCast(ud orelse return));
+ const surface = self.actionSurface() orelse return;
+ _ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| {
+ log.warn("error performing binding action error={}", .{err});
+ return;
+ };
+}
+
fn gtkActionSplitRight(
_: *c.GSimpleAction,
_: *c.GVariant,
@@ -1045,8 +1051,21 @@ fn gtkActionReset(
};
}
+fn gtkActionClear(
+ _: *c.GSimpleAction,
+ _: *c.GVariant,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const self: *Window = @ptrCast(@alignCast(ud orelse return));
+ const surface = self.actionSurface() orelse return;
+ _ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| {
+ log.warn("error performing binding action error={}", .{err});
+ return;
+ };
+}
+
/// Returns the surface to use for an action.
-fn actionSurface(self: *Window) ?*CoreSurface {
+pub fn actionSurface(self: *Window) ?*CoreSurface {
const tab = self.notebook.currentTab() orelse return null;
const surface = tab.focus_child orelse return null;
return &surface.core_surface;
@@ -1055,3 +1074,17 @@ fn actionSurface(self: *Window) ?*CoreSurface {
fn userdataSelf(ud: *anyopaque) *Window {
return @ptrCast(@alignCast(ud));
}
+
+fn gtkMenuActivate(
+ btn: *c.GtkMenuButton,
+ _: *c.GParamSpec,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const active = c.gtk_menu_button_get_active(btn) != 0;
+ const self = userdataSelf(ud orelse return);
+ if (active) {
+ self.menu.refresh();
+ } else {
+ self.focusCurrentTab();
+ }
+}
diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig
index 327680993d..6935619b86 100644
--- a/src/apprt/gtk/gresource.zig
+++ b/src/apprt/gtk/gresource.zig
@@ -53,6 +53,11 @@ const icons = [_]struct {
},
};
+const builder_files = [_][]const u8{
+ "menu-window.ui",
+ "menu-surface.ui",
+};
+
pub const gresource_xml = comptimeGenerateGResourceXML();
fn comptimeGenerateGResourceXML() []const u8 {
@@ -73,9 +78,6 @@ fn writeGResourceXML(writer: anytype) !void {
try writer.writeAll(
\\
\\
- \\
- );
- try writer.writeAll(
\\
\\
);
@@ -87,9 +89,6 @@ fn writeGResourceXML(writer: anytype) !void {
}
try writer.writeAll(
\\
- \\
- );
- try writer.writeAll(
\\
\\
);
@@ -99,6 +98,14 @@ fn writeGResourceXML(writer: anytype) !void {
.{ icon.alias, icon.source },
);
}
+ try writer.writeAll(
+ \\
+ \\
+ \\
+ );
+ for (builder_files) |builder_file| {
+ try writer.print(" src/apprt/gtk/ui/{s}\n", .{ builder_file, builder_file });
+ }
try writer.writeAll(
\\
\\
@@ -107,12 +114,20 @@ fn writeGResourceXML(writer: anytype) !void {
}
pub const dependencies = deps: {
- var deps: [css_files.len + icons.len][]const u8 = undefined;
- for (css_files, 0..) |css_file, i| {
- deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
+ const total = css_files.len + icons.len + builder_files.len;
+ var deps: [total][]const u8 = undefined;
+ var index: usize = 0;
+ for (css_files) |css_file| {
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
+ index += 1;
+ }
+ for (icons) |icon| {
+ deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
+ index += 1;
}
- for (icons, css_files.len..) |icon, i| {
- deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
+ for (builder_files) |builder_file| {
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}", .{builder_file});
+ index += 1;
}
break :deps deps;
};
diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig
new file mode 100644
index 0000000000..f4293e29d9
--- /dev/null
+++ b/src/apprt/gtk/menu.zig
@@ -0,0 +1,110 @@
+const std = @import("std");
+
+const c = @import("c.zig").c;
+const apprt = @import("../../apprt.zig");
+const App = @import("App.zig");
+const Window = @import("Window.zig");
+const Surface = @import("Surface.zig");
+
+const log = std.log.scoped(.gtk_menu);
+
+pub fn Menu(comptime T: type) type {
+ return struct {
+ parent: *T,
+ popover: *c.GtkPopover,
+
+ pub fn init(self: *Menu(T)) void {
+ const name = switch (T) {
+ Window => "window",
+ Surface => "surface",
+ else => unreachable,
+ };
+ const parent: *T = @alignCast(@fieldParentPtr("menu", self));
+
+ const builder = c.gtk_builder_new_from_resource("/com/mitchellh/ghostty/ui/menu-" ++ name ++ ".ui");
+ defer c.g_object_unref(@ptrCast(builder));
+
+ const menu: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu")));
+ const popover: *c.GtkPopover = @ptrCast(@alignCast(c.gtk_popover_menu_new_from_model(menu)));
+ c.gtk_popover_menu_set_flags(@ptrCast(@alignCast(popover)), c.GTK_POPOVER_MENU_NESTED);
+
+ _ = c.g_signal_connect_data(
+ popover,
+ "closed",
+ c.G_CALLBACK(>kRefocusTerm),
+ self,
+ null,
+ c.G_CONNECT_DEFAULT,
+ );
+
+ self.* = .{
+ .parent = parent,
+ .popover = popover,
+ };
+ }
+
+ pub fn setParent(self: *const Menu(T), widget: *c.GtkWidget) void {
+ c.gtk_widget_set_parent(self.asWidget(), widget);
+ }
+
+ pub fn asPopover(self: *const Menu(T)) *c.GtkPopover {
+ return self.popover;
+ }
+
+ pub fn asWidget(self: *const Menu(T)) *c.GtkWidget {
+ return @ptrCast(@alignCast(self.popover));
+ }
+
+ pub fn isVisible(self: *const Menu(T)) bool {
+ return c.gtk_widget_get_visible(self.asWidget()) != 0;
+ }
+
+ pub fn refresh(self: *const Menu(T)) void {
+ const window: *Window, const has_selection: bool = switch (T) {
+ Window => window: {
+ const core_surface = self.parent.actionSurface() orelse break :window .{ self.parent, false };
+ const has_selection = core_surface.hasSelection();
+ break :window .{ self.parent, has_selection };
+ },
+ Surface => surface: {
+ const window = self.parent.container.window() orelse return;
+ const has_selection = self.parent.core_surface.hasSelection();
+ break :surface .{ window, has_selection };
+ },
+ else => unreachable,
+ };
+
+ const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(
+ @ptrCast(@alignCast(window.window)),
+ "copy",
+ ));
+ c.g_simple_action_set_enabled(action, @intFromBool(has_selection));
+ }
+
+ pub fn popupAt(self: *const Menu(T), x: f64, y: f64) void {
+ const rect: c.GdkRectangle = .{
+ .x = @intFromFloat(x),
+ .y = @intFromFloat(y),
+ .width = 1,
+ .height = 1,
+ };
+ c.gtk_popover_set_pointing_to(self.popover, &rect);
+ self.refresh();
+ c.gtk_popover_popup(self.popover);
+ }
+
+ fn gtkRefocusTerm(_: *c.GtkPopover, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) bool {
+ const self: *Menu(T) = @ptrCast(@alignCast(ud orelse return false));
+
+ const window: *Window = switch (T) {
+ Window => self.parent,
+ Surface => self.parent.container.window() orelse return false,
+ else => unreachable,
+ };
+
+ window.focusCurrentTab();
+
+ return true;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/menu-surface.ui b/src/apprt/gtk/ui/menu-surface.ui
new file mode 100644
index 0000000000..9345e0aea2
--- /dev/null
+++ b/src/apprt/gtk/ui/menu-surface.ui
@@ -0,0 +1,93 @@
+
+
+
+
+
diff --git a/src/apprt/gtk/ui/menu-window.ui b/src/apprt/gtk/ui/menu-window.ui
new file mode 100644
index 0000000000..9345e0aea2
--- /dev/null
+++ b/src/apprt/gtk/ui/menu-window.ui
@@ -0,0 +1,93 @@
+
+
+
+
+