diff --git a/include/ghostty.h b/include/ghostty.h index 3189781f7..d64a01f48 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -571,7 +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_TOGGLE_MENUBAR, GHOSTTY_ACTION_MOVE_TAB, GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_SPLIT, diff --git a/src/Surface.zig b/src/Surface.zig index ddcfb3035..543e5ca8f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4213,9 +4213,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .toggle, ), - .toggle_top_menu => try self.rt_app.performAction( + .toggle_menubar => try self.rt_app.performAction( .{ .surface = self }, - .toggle_top_menu, + .toggle_menubar, {}, ), diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 0414ebe78..98ad6f2c6 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -110,8 +110,8 @@ 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, + /// Toggle whether the window menubar is shown. + toggle_menubar, /// Moves a tab by a relative offset. /// @@ -243,7 +243,7 @@ pub const Action = union(Key) { toggle_window_decorations, toggle_quick_terminal, toggle_visibility, - toggle_top_menu, + toggle_menubar, move_tab, goto_tab, goto_split, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 5ef8e73df..e3d527f72 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -223,7 +223,7 @@ pub const App = struct { .toggle_window_decorations, .toggle_quick_terminal, .toggle_visibility, - .toggle_top_menu, + .toggle_menubar, .goto_tab, .move_tab, .inspector, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 5712ad1ac..a3d24d8f7 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -506,7 +506,7 @@ pub fn performAction( }), .toggle_maximize => self.toggleMaximize(target), .toggle_fullscreen => self.toggleFullscreen(target, value), - .toggle_top_menu => self.toggleTopMenu(target), + .toggle_menubar => self.toggleMenubar(target), .new_tab => try self.newTab(target), .close_tab => try self.closeTab(target), @@ -789,18 +789,18 @@ fn toggleWindowDecorations( } } -fn toggleTopMenu(_: *App, target: apprt.Target) void { +fn toggleMenubar(_: *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}", + "toggleMenubar invalid for container={s}", .{@tagName(v.rt_surface.container)}, ); return; }; - window.toggleTopMenu(); + window.toggleMenubar(); }, } } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7866877e3..bff1529d5 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -380,7 +380,7 @@ im_len: u7 = 0, cgroup_path: ?[]const u8 = null, /// Our context menu. -context_menu: Menu(Surface, .context, .popover_menu), +context_menu: Menu(Surface, "context_menu", .popover_menu), /// Configuration used for initializing the surface. We have to copy some /// data since initialization is delayed with GTK (on realize). diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index a2a31c385..bfccc7072 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -48,14 +48,14 @@ tab_overview: ?*c.GtkWidget, /// can be either c.GtkNotebook or c.AdwTabView. notebook: Notebook, -/// The "top" menu that appears at the top of a window. -top_menu: Menu(Window, .top, .popover_menu_bar), +/// Menu that appears at the top of a window inbetween the titlebar and tabbar. +menubar: Menu(Window, "menubar", .popover_menu_bar), /// Revealer for showing/hiding top menu. -top_menu_revealer: *c.GtkRevealer, +menubar_revealer: *c.GtkRevealer, /// The "main" menu that is attached to a button in the headerbar. -titlebar_menu: Menu(Window, .titlebar, .popover_menu), +titlebar_menu: Menu(Window, "titlebar_menu", .popover_menu), /// The libadwaita widget for receiving toast send requests. If libadwaita is /// not used, this is null and unused. @@ -104,8 +104,8 @@ pub fn init(self: *Window, app: *App) !void { .tab_overview = null, .toast_overlay = null, .notebook = undefined, - .top_menu = undefined, - .top_menu_revealer = undefined, + .menubar = undefined, + .menubar_revealer = undefined, .titlebar_menu = undefined, .winproto = .none, }; @@ -149,12 +149,12 @@ pub fn init(self: *Window, app: *App) !void { const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); // Set up the menus - self.top_menu.init(); + self.menubar.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); + self.menubar_revealer = @ptrCast(@alignCast(c.gtk_revealer_new())); + c.gtk_revealer_set_child(self.menubar_revealer, self.menubar.asWidget()); + c.gtk_revealer_set_transition_type(self.menubar_revealer, c.GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); // Setup our notebook self.notebook.init(); @@ -200,7 +200,7 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data( btn, "notify::active", - c.G_CALLBACK(>kMenuActivate), + c.G_CALLBACK(>kTitlebarMenuActivate), self, null, c.G_CONNECT_DEFAULT, @@ -258,11 +258,11 @@ pub fn init(self: *Window, app: *App) !void { .adw140 => {}, .adw, .adw130 => { c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget()); - c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer))); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.menubar_revealer))); }, .gtk => { c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget()); - c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer))); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.menubar_revealer))); }, } @@ -344,7 +344,7 @@ pub fn init(self: *Window, app: *App) !void { 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))); + c.gtk_box_append(@ptrCast(top_box), @ptrCast(@alignCast(self.menubar_revealer))); c.adw_toolbar_view_add_top_bar(toolbar_view, top_box); @@ -644,9 +644,9 @@ pub fn toggleWindowDecorations(self: *Window) void { } /// 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)); +pub fn toggleMenubar(self: *Window) void { + const is_revealed = c.gtk_revealer_get_reveal_child(self.menubar_revealer) != 0; + c.gtk_revealer_set_reveal_child(self.menubar_revealer, @intFromBool(!is_revealed)); } /// Grabs focus on the currently selected tab. @@ -1149,7 +1149,7 @@ fn userdataSelf(ud: *anyopaque) *Window { return @ptrCast(@alignCast(ud)); } -fn gtkMenuActivate( +fn gtkTitlebarMenuActivate( btn: *c.GtkMenuButton, _: *c.GParamSpec, ud: ?*anyopaque, diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig new file mode 100644 index 000000000..7952bf709 --- /dev/null +++ b/src/apprt/gtk/builder_check.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub const c = @cImport({ + @cInclude("gtk/gtk.h"); +}); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const alloc = gpa.allocator(); + + const filename = filename: { + var it = try std.process.argsWithAllocator(alloc); + defer it.deinit(); + + _ = it.next() orelse return error.NoFilename; + break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename); + }; + defer alloc.free(filename); + + c.gtk_init(); + + const builder = c.gtk_builder_new_from_file(filename.ptr); + defer c.g_object_unref(builder); +} diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 0b46a4e4f..7ab6db61b 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -53,6 +53,12 @@ const icons = [_]struct { }, }; +pub const ui_files = [_][]const u8{ + "menu-surface-context_menu", + "menu-window-menubar", + "menu-window-titlebar_menu", +}; + pub const gresource_xml = comptimeGenerateGResourceXML(); fn comptimeGenerateGResourceXML() []const u8 { @@ -93,6 +99,17 @@ fn writeGResourceXML(writer: anytype) !void { .{ icon.alias, icon.source }, ); } + try writer.writeAll( + \\ + \\ + \\ + ); + for (ui_files) |ui_file| { + try writer.print( + " src/apprt/gtk/ui/{0s}.ui\n", + .{ui_file}, + ); + } try writer.writeAll( \\ \\ @@ -101,7 +118,7 @@ fn writeGResourceXML(writer: anytype) !void { } pub const dependencies = deps: { - const total = css_files.len + icons.len; + const total = css_files.len + icons.len + ui_files.len; var deps: [total][]const u8 = undefined; var index: usize = 0; for (css_files) |css_file| { @@ -112,5 +129,9 @@ pub const dependencies = deps: { deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); index += 1; } + for (ui_files) |ui_file| { + deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.ui", .{ui_file}); + index += 1; + } break :deps deps; }; diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig index 00ef92a13..880e3dd3a 100644 --- a/src/apprt/gtk/menu.zig +++ b/src/apprt/gtk/menu.zig @@ -10,7 +10,7 @@ const log = std.log.scoped(.gtk_menu); pub fn Menu( comptime T: type, - comptime variant: enum { top, titlebar, context }, + comptime variant: []const u8, comptime style: enum { popover_menu, popover_menu_bar }, ) type { return struct { @@ -29,12 +29,27 @@ pub fn Menu( Surface => "surface", else => unreachable, }; - const parent: *T = @alignCast(@fieldParentPtr(@tagName(variant) ++ "_menu", self)); + const parent: *T = @alignCast(@fieldParentPtr(variant, 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)); + const path = "ui/menu-" ++ name ++ "-" ++ variant ++ ".ui"; + + comptime { + // Use @embedFile to make sure that the file exists at compile + // time. Zig _should_ discard the data so that it doesn't end up + // in the final executable. At runtime we will load the data from + // a GResource. + _ = @embedFile(path); + + // Check to make sure that our file is listed as a `ui_file` in + // `gresource.zig`. If it isn't Ghostty could crash at runtime + // when we try and load a nonexistent GResource. + const gresource = @import("gresource.zig"); + for (gresource.ui_files) |ui_file| { + if (std.mem.eql(u8, ui_file, "menu-" ++ name ++ "-" ++ variant)) break; + } else @compileError("missing 'menu-" ++ name ++ "-" ++ variant ++ "' in gresource.zig"); + } + + const builder = c.gtk_builder_new_from_resource("/com/mitchellh/ghostty/" ++ path); defer c.g_object_unref(@ptrCast(builder)); const menu_model: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu"))); diff --git a/src/apprt/gtk/ui/menu-window-titlebar.ui b/src/apprt/gtk/ui/menu-surface-context_menu.ui similarity index 52% rename from src/apprt/gtk/ui/menu-window-titlebar.ui rename to src/apprt/gtk/ui/menu-surface-context_menu.ui index 9345e0aea..37d07bec2 100644 --- a/src/apprt/gtk/ui/menu-window-titlebar.ui +++ b/src/apprt/gtk/ui/menu-surface-context_menu.ui @@ -14,22 +14,12 @@
- New Window - win.new-window + Clear + win.clear - Close Window - win.close - -
-
- - New Tab - win.new-tab - - - Close Tab - win.close-tab + Reset + win.reset
@@ -54,40 +44,47 @@
+ + Tab +
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Window +
+ + New Window + win.new-window + + + Close Window + win.close + +
+
- - 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 - + + Config +
+ + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
diff --git a/src/apprt/gtk/ui/menu-window-top.ui b/src/apprt/gtk/ui/menu-window-menubar.ui similarity index 100% rename from src/apprt/gtk/ui/menu-window-top.ui rename to src/apprt/gtk/ui/menu-window-menubar.ui diff --git a/src/apprt/gtk/ui/menu-surface-context.ui b/src/apprt/gtk/ui/menu-window-titlebar_menu.ui similarity index 100% rename from src/apprt/gtk/ui/menu-surface-context.ui rename to src/apprt/gtk/ui/menu-window-titlebar_menu.ui diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 64068658d..6fe11147f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -469,6 +469,24 @@ pub fn add( const wf = b.addWriteFiles(); const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml); + { + const builder_check = b.addExecutable(.{ + .name = "builder_check", + .root_source_file = b.path("src/apprt/gtk/builder_check.zig"), + .target = b.host, + }); + builder_check.linkSystemLibrary2("gtk4", dynamic_link_opts); + builder_check.linkLibC(); + + for (gresource.dependencies) |pathname| { + const extension = std.fs.path.extension(pathname); + if (!std.mem.eql(u8, extension, ".ui")) continue; + const check = b.addRunArtifact(builder_check); + check.addFileArg(b.path(pathname)); + step.step.dependOn(&check.step); + } + } + const generate_resources_c = b.addSystemCommand(&.{ "glib-compile-resources", "--c-name", diff --git a/src/input/Binding.zig b/src/input/Binding.zig index f35e70be0..84c26fc4c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -473,9 +473,9 @@ 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, + /// Show/hide the window menu that appears below the titlebar and above the + /// tab bar. + toggle_menubar: void, /// Quit ghostty. quit: void, @@ -784,7 +784,7 @@ pub const Action = union(enum) { .goto_tab, .move_tab, .toggle_tab_overview, - .toggle_top_menu, + .toggle_menubar, .new_split, .goto_split, .toggle_split_zoom,