Skip to content

Commit

Permalink
gtk: rework Windows and menus
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jcollie committed Jan 25, 2025
1 parent e2e6770 commit 8f7d0ee
Show file tree
Hide file tree
Showing 13 changed files with 788 additions and 349 deletions.
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_TOP_MENU,
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_top_menu => try self.rt_app.performAction(
.{ .surface = self },
.toggle_top_menu,
{},
),

.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 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
Expand Down Expand Up @@ -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,
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_top_menu,
.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_top_menu => self.toggleTopMenu(target),

.new_tab => try self.newTab(target),
.close_tab => try self.closeTab(target),
Expand Down Expand Up @@ -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(),
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
49 changes: 13 additions & 36 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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", .{});

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

Expand Down Expand Up @@ -2030,15 +2008,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();
Expand Down
Loading

0 comments on commit 8f7d0ee

Please sign in to comment.