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,