Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gtk: rework Windows and menus #4952

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ typedef enum {
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
GHOSTTY_ACTION_TOGGLE_MENUBAR,
GHOSTTY_ACTION_MOVE_TAB,
GHOSTTY_ACTION_GOTO_TAB,
GHOSTTY_ACTION_GOTO_SPLIT,
Expand Down
6 changes: 6 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4213,6 +4213,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.toggle,
),

.toggle_menubar => try self.rt_app.performAction(
.{ .surface = self },
.toggle_menubar,
{},
),

.select_all => {
const sel = self.io.terminal.screen.selectAll();
if (sel) |s| {
Expand Down
4 changes: 4 additions & 0 deletions src/apprt/action.zig
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ pub const Action = union(Key) {
/// Toggle the visibility of all Ghostty terminal windows.
toggle_visibility,

/// Toggle whether the window menubar is shown.
toggle_menubar,

/// Moves a tab by a relative offset.
///
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1
Expand Down Expand Up @@ -240,6 +243,7 @@ pub const Action = union(Key) {
toggle_window_decorations,
toggle_quick_terminal,
toggle_visibility,
toggle_menubar,
move_tab,
goto_tab,
goto_split,
Expand Down
1 change: 1 addition & 0 deletions src/apprt/glfw.zig
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ pub const App = struct {
.toggle_window_decorations,
.toggle_quick_terminal,
.toggle_visibility,
.toggle_menubar,
.goto_tab,
.move_tab,
.inspector,
Expand Down
139 changes: 36 additions & 103 deletions src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -514,6 +506,7 @@ pub fn performAction(
}),
.toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value),
.toggle_menubar => self.toggleMenubar(target),

.new_tab => try self.newTab(target),
.close_tab => try self.closeTab(target),
Expand Down Expand Up @@ -796,6 +789,21 @@ fn toggleWindowDecorations(
}
}

fn toggleMenubar(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"toggleMenubar invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
window.toggleMenubar();
},
}
}
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
switch (mode) {
.start => self.startQuitTimer(),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions src/apprt/gtk/Builder.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// Wrapper around GTK's builder APIs that perform some comptime checks.
const Builder = @This();

const std = @import("std");
const c = @import("c.zig").c;

builder: *c.GtkBuilder,

pub fn init(comptime name: []const u8) Builder {
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("ui/" ++ name ++ ".ui");

// 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, name)) break;
} else @compileError("missing '" ++ name ++ "' in gresource.zig");
}

return .{
.builder = c.gtk_builder_new_from_resource("/com/mitchellh/ghostty/ui/" ++ name ++ ".ui") orelse unreachable,
};
}

pub fn getObject(self: *const Builder, comptime T: type, name: [:0]const u8) *T {
return @ptrCast(@alignCast(c.gtk_builder_get_object(self.builder, name.ptr)));
}

pub fn deinit(self: *const Builder) void {
c.g_object_unref(self.builder);
}
Loading
Loading