mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
gtk: rework Windows and menus
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.
This commit is contained in:
@ -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,
|
||||
|
@ -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| {
|
||||
|
@ -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,
|
||||
|
@ -223,6 +223,7 @@ pub const App = struct {
|
||||
.toggle_window_decorations,
|
||||
.toggle_quick_terminal,
|
||||
.toggle_visibility,
|
||||
.toggle_top_menu,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.inspector,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -73,9 +73,6 @@ fn writeGResourceXML(writer: anytype) !void {
|
||||
try writer.writeAll(
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<gresources>
|
||||
\\
|
||||
);
|
||||
try writer.writeAll(
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty">
|
||||
\\
|
||||
);
|
||||
@ -87,9 +84,6 @@ fn writeGResourceXML(writer: anytype) !void {
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\
|
||||
);
|
||||
try writer.writeAll(
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
||||
\\
|
||||
);
|
||||
@ -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;
|
||||
};
|
||||
|
138
src/apprt/gtk/menu.zig
Normal file
138
src/apprt/gtk/menu.zig
Normal file
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
93
src/apprt/gtk/ui/menu-surface-context.ui
Normal file
93
src/apprt/gtk/ui/menu-surface-context.ui
Normal file
@ -0,0 +1,93 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<interface domain="com.mitchellh.ghostty">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<menu id="menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Copy</attribute>
|
||||
<attribute name="action">win.copy</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Paste</attribute>
|
||||
<attribute name="action">win.paste</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<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>
|
||||
<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>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label">Split</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Up</attribute>
|
||||
<attribute name="action">win.split-up</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Down</attribute>
|
||||
<attribute name="action">win.split-down</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Left</attribute>
|
||||
<attribute name="action">win.split-left</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Right</attribute>
|
||||
<attribute name="action">win.split-right</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>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
93
src/apprt/gtk/ui/menu-window-titlebar.ui
Normal file
93
src/apprt/gtk/ui/menu-window-titlebar.ui
Normal file
@ -0,0 +1,93 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<interface domain="com.mitchellh.ghostty">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<menu id="menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Copy</attribute>
|
||||
<attribute name="action">win.copy</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Paste</attribute>
|
||||
<attribute name="action">win.paste</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<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>
|
||||
<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>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label">Split</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Up</attribute>
|
||||
<attribute name="action">win.split-up</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Down</attribute>
|
||||
<attribute name="action">win.split-down</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Left</attribute>
|
||||
<attribute name="action">win.split-left</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Right</attribute>
|
||||
<attribute name="action">win.split-right</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>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
106
src/apprt/gtk/ui/menu-window-top.ui
Normal file
106
src/apprt/gtk/ui/menu-window-top.ui
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<interface domain="com.mitchellh.ghostty">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<menu id="menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_File</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>
|
||||
<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>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label">Split</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Up</attribute>
|
||||
<attribute name="action">win.split-up</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Down</attribute>
|
||||
<attribute name="action">win.split-down</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Left</attribute>
|
||||
<attribute name="action">win.split-left</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Split Right</attribute>
|
||||
<attribute name="action">win.split-right</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Quit</attribute>
|
||||
<attribute name="action">app.quit</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_Edit</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Copy</attribute>
|
||||
<attribute name="action">win.copy</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Paste</attribute>
|
||||
<attribute name="action">win.paste</attribute>
|
||||
</item>
|
||||
</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">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>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">_Help</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Terminal Inspector</attribute>
|
||||
<attribute name="action">win.toggle-inspector</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About Ghostty</attribute>
|
||||
<attribute name="action">win.about</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
</interface>
|
@ -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,
|
||||
|
Reference in New Issue
Block a user