From aff25418fe799a592f61feb92e432eaacf1679bd Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 21 Jan 2025 14:44:11 -0600 Subject: [PATCH] gtk: rework Windows and menus 1. Rework the GTK Window code to clean up the if/else spaghetti. This _should_ fix issues with older versions of Adwaita getting the titlebar and tab bar out of order. 2. Consolidate code for menus into one file and switch to using GtkPopupMenus built from GTK Builder XML files. This changes menus so that there is one per window and one per surface. This results in more memory usage, but more correct behavior. Previously context menus would pop up at the wrong location, due to not being attached to the correct GTK widget. Using GTK Builder XML files reduces the amount of code to create the menus and will make future changes to the menu structure easier. 3. Add a "top menu" that can be shown/hidden with a keybind action. This will be useful for people that use SSD and thus don't have the hamburger menu from the title bar. --- include/ghostty.h | 1 + src/Surface.zig | 6 + src/apprt/action.zig | 4 + src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 139 ++----- src/apprt/gtk/Surface.zig | 49 +-- src/apprt/gtk/Window.zig | 474 ++++++++++++++--------- src/apprt/gtk/gresource.zig | 20 +- src/apprt/gtk/menu.zig | 138 +++++++ src/apprt/gtk/ui/menu-surface-context.ui | 93 +++++ src/apprt/gtk/ui/menu-window-titlebar.ui | 93 +++++ src/apprt/gtk/ui/menu-window-top.ui | 106 +++++ src/input/Binding.zig | 5 + 13 files changed, 786 insertions(+), 343 deletions(-) create mode 100644 src/apprt/gtk/menu.zig create mode 100644 src/apprt/gtk/ui/menu-surface-context.ui create mode 100644 src/apprt/gtk/ui/menu-window-titlebar.ui create mode 100644 src/apprt/gtk/ui/menu-window-top.ui diff --git a/include/ghostty.h b/include/ghostty.h index 246fb9ed3..3189781f7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -571,6 +571,7 @@ typedef enum { GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, GHOSTTY_ACTION_TOGGLE_VISIBILITY, + GHOSTTY_ACTION_TOGGLE_TOP_MENU, GHOSTTY_ACTION_MOVE_TAB, GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_SPLIT, diff --git a/src/Surface.zig b/src/Surface.zig index d9a985aa7..ddcfb3035 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4213,6 +4213,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .toggle, ), + .toggle_top_menu => try self.rt_app.performAction( + .{ .surface = self }, + .toggle_top_menu, + {}, + ), + .select_all => { const sel = self.io.terminal.screen.selectAll(); if (sel) |s| { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index fe2039e52..0414ebe78 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -110,6 +110,9 @@ pub const Action = union(Key) { /// Toggle the visibility of all Ghostty terminal windows. toggle_visibility, + /// Toggle whether the top menu is shown. + toggle_top_menu, + /// Moves a tab by a relative offset. /// /// Adjusts the tab position based on `offset` (e.g., -1 for left, +1 @@ -240,6 +243,7 @@ pub const Action = union(Key) { toggle_window_decorations, toggle_quick_terminal, toggle_visibility, + toggle_top_menu, move_tab, goto_tab, goto_split, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 686a70ddb..5ef8e73df 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -223,6 +223,7 @@ pub const App = struct { .toggle_window_decorations, .toggle_quick_terminal, .toggle_visibility, + .toggle_top_menu, .goto_tab, .move_tab, .inspector, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index df74cefb2..5712ad1ac 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, @@ -485,8 +479,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| { @@ -514,6 +506,7 @@ pub fn performAction( }), .toggle_maximize => self.toggleMaximize(target), .toggle_fullscreen => self.toggleFullscreen(target, value), + .toggle_top_menu => self.toggleTopMenu(target), .new_tab => try self.newTab(target), .close_tab => try self.closeTab(target), @@ -796,6 +789,21 @@ fn toggleWindowDecorations( } } +fn toggleTopMenu(_: *App, target: apprt.Target) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "toggleTopMenu invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + window.toggleTopMenu(); + }, + } +} fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), @@ -1035,20 +1043,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( @@ -1280,10 +1296,8 @@ pub fn run(self: *App) !void { // and asynchronously request the initial color scheme self.initDbus(); - // Setup our menu items + // Setup our actions self.initActions(); - self.initMenu(); - self.initContextMenu(); // On startup, we want to check for configuration errors right away // so we can show our error window. We also need to setup other initial @@ -1801,87 +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"); - } - - 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 1ca39425b..7866877e3 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"); @@ -378,6 +379,9 @@ im_len: u7 = 0, /// details on what this is. cgroup_path: ?[]const u8 = null, +/// Our context menu. +context_menu: Menu(Surface, .context, .popover_menu), + /// Configuration used for initializing the surface. We have to copy some /// data since initialization is delayed with GTK (on realize). pub const InitConfig = struct { @@ -562,9 +566,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .cursor_pos = .{ .x = -1, .y = -1 }, .im_context = im_context, .cgroup_path = cgroup_path, + .context_menu = undefined, }; errdefer self.* = undefined; + // initialize the context menu + self.context_menu.init(); + self.context_menu.setParent(overlay); + // Set our default mouse shape try self.setMouseShape(.text); @@ -1210,6 +1219,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; } @@ -1247,38 +1257,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", .{}); @@ -1449,7 +1427,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.context_menu.popupAt(x, y); } } @@ -2031,15 +2009,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.context_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 3a72e1752..a2a31c385 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,14 @@ tab_overview: ?*c.GtkWidget, /// can be either c.GtkNotebook or c.AdwTabView. notebook: Notebook, -context_menu: *c.GtkWidget, +/// The "top" menu that appears at the top of a window. +top_menu: Menu(Window, .top, .popover_menu_bar), + +/// Revealer for showing/hiding top menu. +top_menu_revealer: *c.GtkRevealer, + +/// The "main" menu that is attached to a button in the headerbar. +titlebar_menu: Menu(Window, .titlebar, .popover_menu), /// The libadwaita widget for receiving toast send requests. If libadwaita is /// not used, this is null and unused. @@ -73,6 +81,20 @@ pub fn create(alloc: Allocator, app: *App) !*Window { return window; } +pub const Flavor = enum { + gtk, + adw, + adw130, + adw140, +}; + +pub inline fn flavor(self: *const Window) Flavor { + if (adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) return .adw140; + if (adwaita.versionAtLeast(1, 3, 0) and adwaita.enabled(&self.app.config)) return .adw130; + if (adwaita.versionAtLeast(0, 0, 0) and adwaita.enabled(&self.app.config)) return .adw; + return .gtk; +} + pub fn init(self: *Window, app: *App) !void { // Set up our own state self.* = .{ @@ -80,23 +102,26 @@ pub fn init(self: *Window, app: *App) !void { .window = undefined, .headerbar = undefined, .tab_overview = null, + .toast_overlay = null, .notebook = undefined, - .context_menu = undefined, - .toast_overlay = undefined, + .top_menu = undefined, + .top_menu_revealer = undefined, + .titlebar_menu = undefined, .winproto = .none, }; // Create the window - const window: *c.GtkWidget = window: { - if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) { + const window: *c.GtkWidget = switch (self.flavor()) { + .adw, .adw130, .adw140 => window: { const window = c.adw_application_window_new(app.app); c.gtk_widget_add_css_class(@ptrCast(window), "adw"); break :window window; - } else { + }, + .gtk => window: { const window = c.gtk_application_window_new(app.app); c.gtk_widget_add_css_class(@ptrCast(window), "gtk"); break :window window; - } + }, }; errdefer c.gtk_window_destroy(@ptrCast(window)); @@ -123,33 +148,44 @@ 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 menus + self.top_menu.init(); + self.titlebar_menu.init(); + + self.top_menu_revealer = @ptrCast(@alignCast(c.gtk_revealer_new())); + c.gtk_revealer_set_child(self.top_menu_revealer, self.top_menu.asWidget()); + c.gtk_revealer_set_transition_type(self.top_menu_revealer, c.GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + // Setup our notebook self.notebook.init(); - // If we are using Adwaita, then we can support the tab overview. - self.tab_overview = if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&self.app.config) and adwaita.versionAtLeast(1, 4, 0)) overview: { - const tab_overview = c.adw_tab_overview_new(); - c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); - c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); - _ = c.g_signal_connect_data( - tab_overview, - "create-tab", - c.G_CALLBACK(>kNewTabFromOverview), - self, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - tab_overview, - "notify::open", - c.G_CALLBACK(&adwTabOverviewOpen), - self, - null, - c.G_CONNECT_DEFAULT, - ); + self.tab_overview = switch (self.flavor()) { + .adw140 => overview: { + // If we are using Adwaita 1.4.0, then we can support the tab overview. + const tab_overview = c.adw_tab_overview_new(); + c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); + c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); + _ = c.g_signal_connect_data( + tab_overview, + "create-tab", + c.G_CALLBACK(>kNewTabFromOverview), + self, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + tab_overview, + "notify::open", + c.G_CALLBACK(&adwTabOverviewOpen), + self, + null, + c.G_CONNECT_DEFAULT, + ); - break :overview tab_overview; - } else null; + break :overview tab_overview; + }, + .adw, .adw130, .gtk => null, + }; // gtk-titlebar can be used to disable the header bar (but keep the window // manager's decorations). We create this no matter if we are decorated or @@ -160,40 +196,49 @@ 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.titlebar_menu.asWidget()); + _ = c.g_signal_connect_data( + btn, + "notify::active", + c.G_CALLBACK(>kMenuActivate), + self, + null, + c.G_CONNECT_DEFAULT, + ); self.headerbar.packEnd(btn); } // If we're using an AdwWindow then we can support the tab overview. if (self.tab_overview) |tab_overview| { - if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable; - assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0)); - const btn = switch (app.config.@"gtk-tabs-location") { - .top, .bottom, .left, .right => btn: { - const btn = c.gtk_toggle_button_new(); - c.gtk_widget_set_tooltip_text(btn, "View Open Tabs"); - c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); - _ = c.g_object_bind_property( - btn, - "active", - tab_overview, - "open", - c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE, - ); + switch (self.flavor()) { + .adw140 => { + const btn = switch (app.config.@"gtk-tabs-location") { + .top, .bottom, .left, .right => btn: { + const btn = c.gtk_toggle_button_new(); + c.gtk_widget_set_tooltip_text(btn, "View Open Tabs"); + c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); + _ = c.g_object_bind_property( + btn, + "active", + tab_overview, + "open", + c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE, + ); + break :btn btn; + }, + .hidden => btn: { + const btn = c.adw_tab_button_new(); + c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view); + c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open"); + break :btn btn; + }, + }; - break :btn btn; + c.gtk_widget_set_focus_on_click(btn, c.FALSE); + self.headerbar.packEnd(btn); }, - - .hidden => btn: { - const btn = c.adw_tab_button_new(); - c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view); - c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open"); - break :btn btn; - }, - }; - - c.gtk_widget_set_focus_on_click(btn, c.FALSE); - self.headerbar.packEnd(btn); + .gtk, .adw, .adw130 => {}, + } } { @@ -208,9 +253,17 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT); // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we - // need to stick the headerbar into the content box. - if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { - c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget()); + // need to stick the headerbar into the outer content box. + switch (self.flavor()) { + .adw140 => {}, + .adw, .adw130 => { + c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget()); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer))); + }, + .gtk => { + c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget()); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer))); + }, } // In debug we show a warning and apply the 'devel' class to the window. @@ -218,18 +271,18 @@ pub fn init(self: *Window, app: *App) !void { if (comptime std.debug.runtime_safety) { const warning_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded."; - if ((comptime adwaita.versionAtLeast(1, 3, 0)) and - adwaita.enabled(&app.config) and - adwaita.versionAtLeast(1, 3, 0)) - { - const banner = c.adw_banner_new(warning_text); - c.adw_banner_set_revealed(@ptrCast(banner), 1); - c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner)); - } else { - const warning = c.gtk_label_new(warning_text); - c.gtk_widget_set_margin_top(warning, 10); - c.gtk_widget_set_margin_bottom(warning, 10); - c.gtk_box_append(@ptrCast(warning_box), warning); + switch (self.flavor()) { + .adw130, .adw140 => { + const banner = c.adw_banner_new(warning_text); + c.adw_banner_set_revealed(@ptrCast(banner), 1); + c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner)); + }, + .adw, .gtk => { + const warning = c.gtk_label_new(warning_text); + c.gtk_widget_set_margin_top(warning, 10); + c.gtk_widget_set_margin_bottom(warning, 10); + c.gtk_box_append(@ptrCast(warning_box), warning); + }, } c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel"); c.gtk_widget_add_css_class(@ptrCast(warning_box), "background"); @@ -237,31 +290,32 @@ pub fn init(self: *Window, app: *App) !void { } // Setup our toast overlay if we have one - self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: { - const toast_overlay = c.adw_toast_overlay_new(); - c.adw_toast_overlay_set_child( - @ptrCast(toast_overlay), - @ptrCast(@alignCast(self.notebook.asWidget())), - ); - c.gtk_box_append(@ptrCast(box), toast_overlay); - break :toast toast_overlay; - } else toast: { - c.gtk_box_append(@ptrCast(box), self.notebook.asWidget()); - break :toast null; + self.toast_overlay = switch (self.flavor()) { + .adw, .adw130, .adw140 => overlay: { + const toast_overlay = c.adw_toast_overlay_new(); + c.adw_toast_overlay_set_child( + @ptrCast(toast_overlay), + @ptrCast(@alignCast(self.notebook.asWidget())), + ); + c.gtk_box_append(@ptrCast(box), toast_overlay); + break :overlay toast_overlay; + }, + .gtk => overlay: { + c.gtk_box_append(@ptrCast(box), self.notebook.asWidget()); + break :overlay null; + }, }; // If we have a tab overview then we can set it on our notebook. if (self.tab_overview) |tab_overview| { - if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable; - assert(self.notebook == .adw); - c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); + switch (self.flavor()) { + .adw130, .adw140 => { + c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); + }, + .adw, .gtk => {}, + } } - 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,91 +330,92 @@ 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); _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); // Our actions for the menu - initActions(self); + self.initActions(); - if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { - const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); + switch (self.flavor()) { + .adw140 => { + const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); - c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget()); + const top_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); + c.gtk_box_append(@ptrCast(top_box), self.headerbar.asWidget()); + c.gtk_box_append(@ptrCast(top_box), @ptrCast(@alignCast(self.top_menu_revealer))); - if (self.app.config.@"gtk-tabs-location" != .hidden) { - const tab_bar = c.adw_tab_bar_new(); - c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view); + c.adw_toolbar_view_add_top_bar(toolbar_view, top_box); - if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); + if (self.app.config.@"gtk-tabs-location" != .hidden) { + const tab_bar = c.adw_tab_bar_new(); + c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view); + c.adw_tab_bar_set_expand_tabs(tab_bar, @intFromBool(app.config.@"gtk-wide-tabs")); - const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar)); - switch (self.app.config.@"gtk-tabs-location") { - // left and right are not supported in libadwaita. - .top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget), - .bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), - .hidden => unreachable, - } - } - c.adw_toolbar_view_set_content(toolbar_view, box); - - const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") { - .flat => c.ADW_TOOLBAR_FLAT, - .raised => c.ADW_TOOLBAR_RAISED, - .@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER, - }; - c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style); - c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style); - - // Set our application window content. - c.adw_tab_overview_set_child( - @ptrCast(self.tab_overview), - @ptrCast(@alignCast(toolbar_view)), - ); - c.adw_application_window_set_content( - @ptrCast(gtk_window), - @ptrCast(@alignCast(self.tab_overview)), - ); - } else tab_bar: { - switch (self.notebook) { - .adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) { - if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar; - // In earlier adwaita versions, we need to add the tabbar manually since we do not use - // an AdwToolbarView. - const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; - c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline"); - switch (app.config.@"gtk-tabs-location") { - .top, - .left, - .right, - => c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))), - - .bottom => c.gtk_box_append( - @ptrCast(box), - @ptrCast(@alignCast(tab_bar)), - ), + const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar)); + switch (self.app.config.@"gtk-tabs-location") { + // left and right are not supported in libadwaita. + .top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget), + .bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), .hidden => unreachable, } - c.adw_tab_bar_set_view(tab_bar, adw.tab_view); + } + c.adw_toolbar_view_set_content(toolbar_view, box); - if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); - }, + const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") { + .flat => c.ADW_TOOLBAR_FLAT, + .raised => c.ADW_TOOLBAR_RAISED, + .@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER, + }; + c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style); + c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style); - .gtk => {}, - } + // Set our application window content. + c.adw_tab_overview_set_child( + @ptrCast(self.tab_overview), + @ptrCast(@alignCast(toolbar_view)), + ); + c.adw_application_window_set_content( + @ptrCast(gtk_window), + @ptrCast(@alignCast(self.tab_overview.?)), + ); + }, + .adw, .adw130 => brk: { + if (app.config.@"gtk-tabs-location" == .hidden) break :brk; + + // In earlier adwaita versions, we need to add the tabbar manually since we do not use + // an AdwToolbarView. + const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; + c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline"); + switch (app.config.@"gtk-tabs-location") { + .top, + .left, + .right, + => c.gtk_box_insert_child_after( + @ptrCast(box), + @ptrCast(@alignCast(tab_bar)), + @ptrCast(@alignCast(self.headerbar.asWidget())), + ), + + .bottom => c.gtk_box_append( + @ptrCast(box), + @ptrCast(@alignCast(tab_bar)), + ), + .hidden => unreachable, + } + c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view); + c.adw_tab_bar_set_expand_tabs(tab_bar, @intFromBool(app.config.@"gtk-wide-tabs")); - // The box is our main child - if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { c.adw_application_window_set_content( @ptrCast(gtk_window), box, ); - } else { - c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget()); + }, + + .gtk => { c.gtk_window_set_child(gtk_window, box); - } + }, } // Show the window @@ -407,19 +462,20 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void { // Disable the title buttons (close, maximize, minimize, ...) // *inside* the tab overview if CSDs are disabled. // We do spare the search button, though. - if ((comptime adwaita.versionAtLeast(1, 4, 0)) and - adwaita.enabled(&self.app.config)) - { - if (self.tab_overview) |tab_overview| { - c.adw_tab_overview_set_show_start_title_buttons( - @ptrCast(tab_overview), - @intFromBool(csd_enabled), - ); - c.adw_tab_overview_set_show_end_title_buttons( - @ptrCast(tab_overview), - @intFromBool(csd_enabled), - ); - } + switch (self.flavor()) { + .adw140 => { + if (self.tab_overview) |tab_overview| { + c.adw_tab_overview_set_show_start_title_buttons( + @ptrCast(tab_overview), + @intFromBool(csd_enabled), + ); + c.adw_tab_overview_set_show_end_title_buttons( + @ptrCast(tab_overview), + @intFromBool(csd_enabled), + ); + } + }, + .gtk, .adw, .adw130 => {}, } } @@ -442,16 +498,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| { @@ -470,8 +528,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| { @@ -587,6 +643,12 @@ pub fn toggleWindowDecorations(self: *Window) void { self.updateConfig(&self.app.config) catch {}; } +/// Toggle top menu. +pub fn toggleTopMenu(self: *Window) void { + const is_revealed = c.gtk_revealer_get_reveal_child(self.top_menu_revealer) != 0; + c.gtk_revealer_set_reveal_child(self.top_menu_revealer, @intFromBool(!is_revealed)); +} + /// Grabs focus on the currently selected tab. pub fn focusCurrentTab(self: *Window) void { const tab = self.notebook.currentTab() orelse return; @@ -761,16 +823,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", .{}); @@ -931,11 +983,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( @@ -960,6 +1008,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, @@ -1064,8 +1125,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; @@ -1074,3 +1148,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.titlebar_menu.refresh(); + } else { + self.focusCurrentTab(); + } +} diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 327680993..0b46a4e4f 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -73,9 +73,6 @@ fn writeGResourceXML(writer: anytype) !void { try writer.writeAll( \\ \\ - \\ - ); - try writer.writeAll( \\ \\ ); @@ -87,9 +84,6 @@ fn writeGResourceXML(writer: anytype) !void { } try writer.writeAll( \\ - \\ - ); - try writer.writeAll( \\ \\ ); @@ -107,12 +101,16 @@ 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; + 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, css_files.len..) |icon, i| { - deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); + for (icons) |icon| { + deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); + index += 1; } break :deps deps; }; diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig new file mode 100644 index 000000000..00ef92a13 --- /dev/null +++ b/src/apprt/gtk/menu.zig @@ -0,0 +1,138 @@ +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, + comptime variant: enum { top, titlebar, context }, + comptime style: enum { popover_menu, popover_menu_bar }, +) type { + return struct { + const Self = @This(); + const MenuWidget = switch (style) { + .popover_menu => c.GtkPopoverMenu, + .popover_menu_bar => c.GtkPopoverMenuBar, + }; + + parent: *T, + menu_widget: *MenuWidget, + + pub fn init(self: *Self) void { + const name = switch (T) { + Window => "window", + Surface => "surface", + else => unreachable, + }; + const parent: *T = @alignCast(@fieldParentPtr(@tagName(variant) ++ "_menu", self)); + + // embed the menu data using Zig @embedFile rather than as a GTK resource so that we get + // compile-time errors if we try and embed a file that doesn't exist + const data = @embedFile("ui/menu-" ++ name ++ "-" ++ @tagName(variant) ++ ".ui"); + const builder = c.gtk_builder_new_from_string(data.ptr, @intCast(data.len)); + defer c.g_object_unref(@ptrCast(builder)); + + const menu_model: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu"))); + + const menu_widget: *MenuWidget = switch (style) { + .popover_menu => brk: { + const menu_widget: *MenuWidget = @ptrCast(@alignCast(c.gtk_popover_menu_new_from_model(menu_model))); + c.gtk_popover_menu_set_flags(menu_widget, c.GTK_POPOVER_MENU_NESTED); + _ = c.g_signal_connect_data( + @ptrCast(@alignCast(menu_widget)), + "closed", + c.G_CALLBACK(>kRefocusTerm), + self, + null, + c.G_CONNECT_DEFAULT, + ); + break :brk menu_widget; + }, + .popover_menu_bar => brk: { + break :brk @ptrCast(@alignCast(c.gtk_popover_menu_bar_new_from_model(menu_model))); + }, + }; + + self.* = .{ + .parent = parent, + .menu_widget = menu_widget, + }; + } + + pub fn setParent(self: *const Self, widget: *c.GtkWidget) void { + c.gtk_widget_set_parent(self.asWidget(), widget); + } + + pub fn asPopover(self: *const Self) *c.GtkPopover { + return @ptrCast(@alignCast(self.menu_widget)); + } + + pub fn asWidget(self: *const Self) *c.GtkWidget { + return @ptrCast(@alignCast(self.menu_widget)); + } + + pub fn isVisible(self: *const Self) bool { + return c.gtk_widget_get_visible(self.asWidget()) != 0; + } + + pub fn setVisible(self: *const Self, visible: bool) void { + return c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible)); + } + + pub fn refresh(self: *const Self) 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 Self, 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.asPopover(), &rect); + self.refresh(); + c.gtk_popover_popup(self.asPopover()); + } + + /// refocus tab that lost focus because of the popover menu + fn gtkRefocusTerm(_: *MenuWidget, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) bool { + const self: *Self = @ptrCast(@alignCast(ud orelse return false)); + + log.info("closed!!!", .{}); + + 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-context.ui b/src/apprt/gtk/ui/menu-surface-context.ui new file mode 100644 index 000000000..9345e0aea --- /dev/null +++ b/src/apprt/gtk/ui/menu-surface-context.ui @@ -0,0 +1,93 @@ + + + + +
+ + Copy + win.copy + + + Paste + win.paste + +
+
+ + New Window + win.new-window + + + Close Window + win.close + +
+
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Split +
+ + Split Up + win.split-up + + + Split Down + win.split-down + + + Split Left + win.split-left + + + Split Right + win.split-right + +
+
+
+
+ + Clear + win.clear + + + Reset + win.reset + +
+
+ + Terminal Inspector + win.toggle-inspector + + + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
+ + About Ghostty + win.about + + + Quit + app.quit + +
+
+
diff --git a/src/apprt/gtk/ui/menu-window-titlebar.ui b/src/apprt/gtk/ui/menu-window-titlebar.ui new file mode 100644 index 000000000..9345e0aea --- /dev/null +++ b/src/apprt/gtk/ui/menu-window-titlebar.ui @@ -0,0 +1,93 @@ + + + + +
+ + Copy + win.copy + + + Paste + win.paste + +
+
+ + New Window + win.new-window + + + Close Window + win.close + +
+
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Split +
+ + Split Up + win.split-up + + + Split Down + win.split-down + + + Split Left + win.split-left + + + Split Right + win.split-right + +
+
+
+
+ + Clear + win.clear + + + Reset + win.reset + +
+
+ + Terminal Inspector + win.toggle-inspector + + + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
+ + About Ghostty + win.about + + + Quit + app.quit + +
+
+
diff --git a/src/apprt/gtk/ui/menu-window-top.ui b/src/apprt/gtk/ui/menu-window-top.ui new file mode 100644 index 000000000..f7f9ea4c3 --- /dev/null +++ b/src/apprt/gtk/ui/menu-window-top.ui @@ -0,0 +1,106 @@ + + + + + + _File +
+ + New Window + win.new-window + + + Close Window + win.close + +
+
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Split +
+ + Split Up + win.split-up + + + Split Down + win.split-down + + + Split Left + win.split-left + + + Split Right + win.split-right + +
+
+
+
+ + Quit + app.quit + +
+
+ + _Edit +
+ + Copy + win.copy + + + Paste + win.paste + +
+
+ + Clear + win.clear + + + Reset + win.reset + +
+
+ + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
+ + _Help +
+ + Terminal Inspector + win.toggle-inspector + +
+
+ + About Ghostty + win.about + +
+
+
+
diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 19c103195..f35e70be0 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -473,6 +473,10 @@ pub const Action = union(enum) { /// This currently only works on macOS. toggle_visibility: void, + /// Show/hide the application menu that appears below the titlebar and above + /// the tab bar. + toggle_top_menu: void, + /// Quit ghostty. quit: void, @@ -780,6 +784,7 @@ pub const Action = union(enum) { .goto_tab, .move_tab, .toggle_tab_overview, + .toggle_top_menu, .new_split, .goto_split, .toggle_split_zoom,