gtk: menubar update

1. "top menu" => "menubar"
2. Slimmed down context menu.
3. More robust compile time checks of GTK builder UI files.
This commit is contained in:
Jeffrey C. Ollie
2025-01-24 21:45:55 -06:00
parent aff25418fe
commit 95a53fff91
15 changed files with 162 additions and 87 deletions

View File

@ -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,

View File

@ -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,
{},
),

View File

@ -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,

View File

@ -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,

View File

@ -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();
},
}
}

View File

@ -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).

View File

@ -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(&gtkMenuActivate),
c.G_CALLBACK(&gtkTitlebarMenuActivate),
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,

View File

@ -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);
}

View File

@ -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(
\\ </gresource>
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
\\
);
for (ui_files) |ui_file| {
try writer.print(
" <file alias=\"{0s}.ui\">src/apprt/gtk/ui/{0s}.ui</file>\n",
.{ui_file},
);
}
try writer.writeAll(
\\ </gresource>
\\</gresources>
@ -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;
};

View File

@ -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")));

View File

@ -14,22 +14,12 @@
</section>
<section>
<item>
<attribute name="label" translatable="yes">New Window</attribute>
<attribute name="action">win.new-window</attribute>
<attribute name="label" translatable="yes">Clear</attribute>
<attribute name="action">win.clear</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Close Window</attribute>
<attribute name="action">win.close</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">New Tab</attribute>
<attribute name="action">win.new-tab</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Close Tab</attribute>
<attribute name="action">win.close-tab</attribute>
<attribute name="label" translatable="yes">Reset</attribute>
<attribute name="action">win.reset</attribute>
</item>
</section>
<section>
@ -54,40 +44,47 @@
</item>
</section>
</submenu>
<submenu>
<attribute name="label">Tab</attribute>
<section>
<item>
<attribute name="label" translatable="yes">New Tab</attribute>
<attribute name="action">win.new-tab</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Close Tab</attribute>
<attribute name="action">win.close-tab</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label">Window</attribute>
<section>
<item>
<attribute name="label" translatable="yes">New Window</attribute>
<attribute name="action">win.new-window</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Close Window</attribute>
<attribute name="action">win.close</attribute>
</item>
</section>
</submenu>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Clear</attribute>
<attribute name="action">win.clear</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Reset</attribute>
<attribute name="action">win.reset</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Terminal Inspector</attribute>
<attribute name="action">win.toggle-inspector</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Open Configuration</attribute>
<attribute name="action">app.open-config</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Reload Configuration</attribute>
<attribute name="action">app.reload-config</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">About Ghostty</attribute>
<attribute name="action">win.about</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Quit</attribute>
<attribute name="action">app.quit</attribute>
</item>
<submenu>
<attribute name="label">Config</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Open Configuration</attribute>
<attribute name="action">app.open-config</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Reload Configuration</attribute>
<attribute name="action">app.reload-config</attribute>
</item>
</section>
</submenu>
</section>
</menu>
</interface>

View File

@ -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",

View File

@ -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,