mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00

Fixes https://github.com/ghostty-org/ghostty/issues/5940 The mentioned problem occurs because when creating a new tab through the tab overview we do not have focus on the terminal area widget, it does not matter if we have a custom title set or not. I think it is just safe to remove this check from the code. I've tested the change and I don't really see a valid use case in which we would not want to set the window title even if we don't have focus on the terminal area.
1149 lines
38 KiB
Zig
1149 lines
38 KiB
Zig
/// A Window is a single, real GTK window that holds terminal surfaces.
|
|
///
|
|
/// A Window always contains a notebook (what GTK calls a tabbed container)
|
|
/// even while no tabs are in use, because a notebook without a tab bar has
|
|
/// no visible UI chrome.
|
|
const Window = @This();
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const Allocator = std.mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
|
|
const gio = @import("gio");
|
|
const glib = @import("glib");
|
|
const gobject = @import("gobject");
|
|
const gtk = @import("gtk");
|
|
|
|
const build_config = @import("../../build_config.zig");
|
|
const configpkg = @import("../../config.zig");
|
|
const font = @import("../../font/main.zig");
|
|
const input = @import("../../input.zig");
|
|
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");
|
|
const gtk_key = @import("key.zig");
|
|
const TabView = @import("TabView.zig");
|
|
const HeaderBar = @import("headerbar.zig");
|
|
const version = @import("version.zig");
|
|
const winproto = @import("winproto.zig");
|
|
|
|
const log = std.log.scoped(.gtk);
|
|
|
|
app: *App,
|
|
|
|
/// Used to deduplicate updateConfig invocations
|
|
last_config: usize,
|
|
|
|
/// Local copy of any configuration
|
|
config: DerivedConfig,
|
|
|
|
/// Our window
|
|
window: *c.GtkWindow,
|
|
|
|
/// The header bar for the window.
|
|
headerbar: HeaderBar,
|
|
|
|
/// The tab overview for the window. This is possibly null since there is no
|
|
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
|
tab_overview: ?*c.GtkWidget,
|
|
|
|
/// The notebook (tab grouping) for this window.
|
|
notebook: TabView,
|
|
|
|
/// The "main" menu that is attached to a button in the headerbar.
|
|
titlebar_menu: Menu(Window, "titlebar_menu", true),
|
|
|
|
/// The libadwaita widget for receiving toast send requests.
|
|
toast_overlay: *c.GtkWidget,
|
|
|
|
/// See adwTabOverviewOpen for why we have this.
|
|
adw_tab_overview_focus_timer: ?c.guint = null,
|
|
|
|
/// State and logic for windowing protocol for a window.
|
|
winproto: winproto.Window,
|
|
|
|
pub const DerivedConfig = struct {
|
|
background_opacity: f64,
|
|
background_blur: configpkg.Config.BackgroundBlur,
|
|
window_theme: configpkg.Config.WindowTheme,
|
|
gtk_titlebar: bool,
|
|
gtk_titlebar_hide_when_maximized: bool,
|
|
gtk_tabs_location: configpkg.Config.GtkTabsLocation,
|
|
gtk_wide_tabs: bool,
|
|
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
|
|
|
|
quick_terminal_position: configpkg.Config.QuickTerminalPosition,
|
|
|
|
maximize: bool,
|
|
fullscreen: bool,
|
|
window_decoration: configpkg.Config.WindowDecoration,
|
|
|
|
pub fn init(config: *const configpkg.Config) DerivedConfig {
|
|
return .{
|
|
.background_opacity = config.@"background-opacity",
|
|
.background_blur = config.@"background-blur",
|
|
.window_theme = config.@"window-theme",
|
|
.gtk_titlebar = config.@"gtk-titlebar",
|
|
.gtk_titlebar_hide_when_maximized = config.@"gtk-titlebar-hide-when-maximized",
|
|
.gtk_tabs_location = config.@"gtk-tabs-location",
|
|
.gtk_wide_tabs = config.@"gtk-wide-tabs",
|
|
.gtk_toolbar_style = config.@"gtk-toolbar-style",
|
|
|
|
.quick_terminal_position = config.@"quick-terminal-position",
|
|
|
|
.maximize = config.maximize,
|
|
.fullscreen = config.fullscreen,
|
|
.window_decoration = config.@"window-decoration",
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn create(alloc: Allocator, app: *App) !*Window {
|
|
// Allocate a fixed pointer for our window. We try to minimize
|
|
// allocations but windows and other GUI requirements are so minimal
|
|
// compared to the steady-state terminal operation so we use heap
|
|
// allocation for this.
|
|
//
|
|
// The allocation is owned by the GtkWindow created. It will be
|
|
// freed when the window is closed.
|
|
var window = try alloc.create(Window);
|
|
errdefer alloc.destroy(window);
|
|
try window.init(app);
|
|
return window;
|
|
}
|
|
|
|
pub fn init(self: *Window, app: *App) !void {
|
|
// Set up our own state
|
|
self.* = .{
|
|
.app = app,
|
|
.last_config = @intFromPtr(&app.config),
|
|
.config = DerivedConfig.init(&app.config),
|
|
.window = undefined,
|
|
.headerbar = undefined,
|
|
.tab_overview = null,
|
|
.notebook = undefined,
|
|
.titlebar_menu = undefined,
|
|
.toast_overlay = undefined,
|
|
.winproto = .none,
|
|
};
|
|
|
|
// Create the window
|
|
const gtk_widget = c.adw_application_window_new(app.app);
|
|
errdefer c.gtk_window_destroy(@ptrCast(gtk_widget));
|
|
|
|
self.window = @ptrCast(@alignCast(gtk_widget));
|
|
|
|
c.gtk_window_set_title(self.window, "Ghostty");
|
|
c.gtk_window_set_default_size(self.window, 1000, 600);
|
|
c.gtk_widget_add_css_class(gtk_widget, "window");
|
|
c.gtk_widget_add_css_class(gtk_widget, "terminal-window");
|
|
|
|
// GTK4 grabs F10 input by default to focus the menubar icon. We want
|
|
// to disable this so that terminal programs can capture F10 (such as htop)
|
|
c.gtk_window_set_handle_menubar_accel(self.window, 0);
|
|
|
|
c.gtk_window_set_icon_name(self.window, build_config.bundle_id);
|
|
|
|
// 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.titlebar_menu.init(self);
|
|
|
|
// Setup our notebook
|
|
self.notebook.init(self);
|
|
|
|
// If we are using Adwaita, then we can support the tab overview.
|
|
self.tab_overview = if (adwaita.versionAtLeast(1, 4, 0)) overview: {
|
|
const tab_overview = c.adw_tab_overview_new();
|
|
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.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;
|
|
|
|
// 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
|
|
// not because we can have a keybind to toggle the decorations.
|
|
self.headerbar.init();
|
|
|
|
{
|
|
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_popover(@ptrCast(btn), @ptrCast(@alignCast(self.titlebar_menu.asWidget())));
|
|
_ = c.g_signal_connect_data(
|
|
btn,
|
|
"notify::active",
|
|
c.G_CALLBACK(>kTitlebarMenuActivate),
|
|
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 (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
|
const btn = switch (self.config.gtk_tabs_location) {
|
|
.top, .bottom => 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), @ptrCast(@alignCast(self.notebook.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);
|
|
}
|
|
|
|
{
|
|
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
|
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
|
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
|
self.headerbar.packStart(btn);
|
|
}
|
|
|
|
_ = c.g_signal_connect_data(self.window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
|
|
_ = c.g_signal_connect_data(self.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)) {
|
|
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
|
|
}
|
|
|
|
// In debug we show a warning and apply the 'devel' class to the window.
|
|
// This is a really common issue where people build from source in debug and performance is really bad.
|
|
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 (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);
|
|
}
|
|
c.gtk_widget_add_css_class(gtk_widget, "devel");
|
|
c.gtk_widget_add_css_class(@ptrCast(warning_box), "background");
|
|
c.gtk_box_append(@ptrCast(box), warning_box);
|
|
}
|
|
|
|
// Setup our toast overlay if we have one
|
|
self.toast_overlay = c.adw_toast_overlay_new();
|
|
c.adw_toast_overlay_set_child(
|
|
@ptrCast(self.toast_overlay),
|
|
@ptrCast(@alignCast(self.notebook.asWidget())),
|
|
);
|
|
c.gtk_box_append(@ptrCast(box), self.toast_overlay);
|
|
|
|
// If we have a tab overview then we can set it on our notebook.
|
|
if (self.tab_overview) |tab_overview| {
|
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
|
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
|
|
}
|
|
|
|
// We register a key event controller with the window so
|
|
// we can catch key events when our surface may not be
|
|
// focused (i.e. when the libadw tab overview is shown).
|
|
const ec_key_press = c.gtk_event_controller_key_new();
|
|
errdefer c.g_object_unref(ec_key_press);
|
|
c.gtk_widget_add_controller(gtk_widget, ec_key_press);
|
|
|
|
// All of our events
|
|
_ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
|
_ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
|
_ = c.g_signal_connect_data(self.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);
|
|
|
|
if (adwaita.versionAtLeast(1, 4, 0)) {
|
|
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
|
|
|
|
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
|
|
|
|
if (self.config.gtk_tabs_location != .hidden) {
|
|
const tab_bar = c.adw_tab_bar_new();
|
|
c.adw_tab_bar_set_view(tab_bar, @ptrCast(@alignCast(self.notebook.tab_view)));
|
|
|
|
if (!self.config.gtk_wide_tabs) c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
|
|
|
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
|
|
switch (self.config.gtk_tabs_location) {
|
|
.top => 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.config.gtk_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_widget),
|
|
@ptrCast(@alignCast(self.tab_overview)),
|
|
);
|
|
} else tab_bar: {
|
|
if (self.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 (self.config.gtk_tabs_location) {
|
|
.top => 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, @ptrCast(@alignCast(self.notebook.tab_view)));
|
|
|
|
if (!self.config.gtk_wide_tabs) c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
|
}
|
|
|
|
// If we want the window to be maximized, we do that here.
|
|
if (self.config.maximize) c.gtk_window_maximize(self.window);
|
|
|
|
// If we are in fullscreen mode, new windows start fullscreen.
|
|
if (self.config.fullscreen) c.gtk_window_fullscreen(self.window);
|
|
}
|
|
|
|
pub fn present(self: *Window) void {
|
|
const window: *gtk.Window = @ptrCast(self.window);
|
|
window.present();
|
|
}
|
|
|
|
pub fn toggleVisibility(self: *Window) void {
|
|
const window: *gtk.Widget = @ptrCast(self.window);
|
|
window.setVisible(@intFromBool(window.isVisible() == 0));
|
|
}
|
|
|
|
pub fn updateConfig(
|
|
self: *Window,
|
|
config: *const configpkg.Config,
|
|
) !void {
|
|
// avoid multiple reconfigs when we have many surfaces contained in this
|
|
// window using the integer value of config as a simple marker to know if
|
|
// we've "seen" this particular config before
|
|
const this_config = @intFromPtr(config);
|
|
if (self.last_config == this_config) return;
|
|
self.last_config = this_config;
|
|
|
|
self.config = DerivedConfig.init(config);
|
|
|
|
// We always resync our appearance whenever the config changes.
|
|
try self.syncAppearance();
|
|
}
|
|
|
|
/// Updates appearance based on config settings. Will be called once upon window
|
|
/// realization, every time the config is reloaded, and every time a window state
|
|
/// is toggled (un-/maximized, un-/fullscreened, window decorations toggled, etc.)
|
|
///
|
|
/// TODO: Many of the initial style settings in `create` could possibly be made
|
|
/// reactive by moving them here.
|
|
pub fn syncAppearance(self: *Window) !void {
|
|
const csd_enabled = self.winproto.clientSideDecorationEnabled();
|
|
c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled));
|
|
|
|
// Fix any artifacting that may occur in window corners. The .ssd CSS
|
|
// class is defined in the GtkWindow documentation:
|
|
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
|
|
// for .ssd is provided by GTK and Adwaita.
|
|
toggleCssClass(@ptrCast(self.window), "csd", csd_enabled);
|
|
toggleCssClass(@ptrCast(self.window), "ssd", !csd_enabled);
|
|
toggleCssClass(@ptrCast(self.window), "no-border-radius", !csd_enabled);
|
|
|
|
self.headerbar.setVisible(visible: {
|
|
// Never display the header bar when CSDs are disabled.
|
|
if (!csd_enabled) break :visible false;
|
|
|
|
// Never display the header bar as a quick terminal.
|
|
if (self.app.quick_terminal == self) break :visible false;
|
|
|
|
// Unconditionally disable the header bar when fullscreened.
|
|
if (self.config.fullscreen) break :visible false;
|
|
|
|
// *Conditionally* disable the header bar when maximized,
|
|
// and gtk-titlebar-hide-when-maximized is set
|
|
if (self.config.maximize and self.config.gtk_titlebar_hide_when_maximized)
|
|
break :visible false;
|
|
|
|
break :visible self.config.gtk_titlebar;
|
|
});
|
|
|
|
toggleCssClass(
|
|
@ptrCast(self.window),
|
|
"background",
|
|
self.config.background_opacity >= 1,
|
|
);
|
|
|
|
// Apply class to color headerbar if window-theme is set to `ghostty` and
|
|
// GTK version is before 4.16. The conditional is because above 4.16
|
|
// we use GTK CSS color variables.
|
|
toggleCssClass(
|
|
@ptrCast(self.window),
|
|
"window-theme-ghostty",
|
|
!version.atLeast(4, 16, 0) and self.config.window_theme == .ghostty,
|
|
);
|
|
|
|
if (self.tab_overview) |tab_overview| {
|
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
|
|
|
// Disable the title buttons (close, maximize, minimize, ...)
|
|
// *inside* the tab overview if CSDs are disabled.
|
|
// We do spare the search button, though.
|
|
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));
|
|
|
|
// Update toolbar view style
|
|
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_tab_overview_get_child(@ptrCast(tab_overview)));
|
|
const toolbar_style: c.AdwToolbarStyle = switch (self.config.gtk_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);
|
|
}
|
|
|
|
self.winproto.syncAppearance() catch |err| {
|
|
log.warn("failed to sync winproto appearance error={}", .{err});
|
|
};
|
|
|
|
if (self.app.quick_terminal == self) {
|
|
self.winproto.syncQuickTerminal() catch |err| {
|
|
log.warn("failed to sync quick terminal appearance error={}", .{err});
|
|
};
|
|
}
|
|
}
|
|
|
|
fn toggleCssClass(
|
|
widget: *c.GtkWidget,
|
|
class: [:0]const u8,
|
|
v: bool,
|
|
) void {
|
|
if (v) {
|
|
c.gtk_widget_add_css_class(widget, class);
|
|
} else {
|
|
c.gtk_widget_remove_css_class(widget, class);
|
|
}
|
|
}
|
|
|
|
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
|
/// menus and such. The menu is defined in App.zig but the action is defined
|
|
/// here. The string name binds them.
|
|
fn initActions(self: *Window) void {
|
|
// FIXME: when rest of file is converted to gobject
|
|
const window: *gtk.ApplicationWindow = @ptrCast(@alignCast(self.window));
|
|
const action_map = window.as(gio.ActionMap);
|
|
const actions = .{
|
|
.{ "about", gtkActionAbout },
|
|
.{ "close", gtkActionClose },
|
|
.{ "new-window", gtkActionNewWindow },
|
|
.{ "new-tab", gtkActionNewTab },
|
|
.{ "close-tab", gtkActionCloseTab },
|
|
.{ "split-right", gtkActionSplitRight },
|
|
.{ "split-down", gtkActionSplitDown },
|
|
.{ "split-left", gtkActionSplitLeft },
|
|
.{ "split-up", gtkActionSplitUp },
|
|
.{ "toggle-inspector", gtkActionToggleInspector },
|
|
.{ "copy", gtkActionCopy },
|
|
.{ "paste", gtkActionPaste },
|
|
.{ "reset", gtkActionReset },
|
|
.{ "clear", gtkActionClear },
|
|
.{ "prompt-title", gtkActionPromptTitle },
|
|
};
|
|
|
|
inline for (actions) |entry| {
|
|
const action = gio.SimpleAction.new(entry[0], null);
|
|
defer action.unref();
|
|
_ = gio.SimpleAction.signals.activate.connect(
|
|
action,
|
|
*Window,
|
|
entry[1],
|
|
self,
|
|
.{},
|
|
);
|
|
action_map.addAction(action.as(gio.Action));
|
|
}
|
|
}
|
|
|
|
pub fn deinit(self: *Window) void {
|
|
self.winproto.deinit(self.app.core_app.alloc);
|
|
|
|
if (self.adw_tab_overview_focus_timer) |timer| {
|
|
_ = c.g_source_remove(timer);
|
|
}
|
|
}
|
|
|
|
/// Set the title of the window.
|
|
pub fn setTitle(self: *Window, title: [:0]const u8) void {
|
|
self.headerbar.setTitle(title);
|
|
}
|
|
|
|
/// Set the subtitle of the window if it has one.
|
|
pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
|
|
self.headerbar.setSubtitle(subtitle);
|
|
}
|
|
|
|
/// Add a new tab to this window.
|
|
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
|
const alloc = self.app.core_app.alloc;
|
|
_ = try Tab.create(alloc, self, parent);
|
|
|
|
// TODO: When this is triggered through a GTK action, the new surface
|
|
// redraws correctly. When it's triggered through keyboard shortcuts, it
|
|
// does not (cursor doesn't blink) unless reactivated by refocusing.
|
|
}
|
|
|
|
/// Close the tab for the given notebook page. This will automatically
|
|
/// handle closing the window if there are no more tabs.
|
|
pub fn closeTab(self: *Window, tab: *Tab) void {
|
|
self.notebook.closeTab(tab);
|
|
}
|
|
|
|
/// Go to the previous tab for a surface.
|
|
pub fn gotoPreviousTab(self: *Window, surface: *Surface) bool {
|
|
const tab = surface.container.tab() orelse {
|
|
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
|
return false;
|
|
};
|
|
if (!self.notebook.gotoPreviousTab(tab)) return false;
|
|
self.focusCurrentTab();
|
|
return true;
|
|
}
|
|
|
|
/// Go to the next tab for a surface.
|
|
pub fn gotoNextTab(self: *Window, surface: *Surface) bool {
|
|
const tab = surface.container.tab() orelse {
|
|
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
|
return false;
|
|
};
|
|
if (!self.notebook.gotoNextTab(tab)) return false;
|
|
self.focusCurrentTab();
|
|
return true;
|
|
}
|
|
|
|
/// Move the current tab for a surface.
|
|
pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
|
|
const tab = surface.container.tab() orelse {
|
|
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
|
return;
|
|
};
|
|
self.notebook.moveTab(tab, position);
|
|
}
|
|
|
|
/// Go to the last tab for a surface.
|
|
pub fn gotoLastTab(self: *Window) bool {
|
|
const max = self.notebook.nPages();
|
|
return self.gotoTab(@intCast(max));
|
|
}
|
|
|
|
/// Go to the specific tab index.
|
|
pub fn gotoTab(self: *Window, n: usize) bool {
|
|
if (n == 0) return false;
|
|
const max = self.notebook.nPages();
|
|
if (max == 0) return false;
|
|
const page_idx = std.math.cast(c_int, n - 1) orelse return false;
|
|
if (!self.notebook.gotoNthTab(@min(page_idx, max - 1))) return false;
|
|
self.focusCurrentTab();
|
|
return true;
|
|
}
|
|
|
|
/// Toggle tab overview (if present)
|
|
pub fn toggleTabOverview(self: *Window) void {
|
|
if (self.tab_overview) |tab_overview_widget| {
|
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
|
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
|
|
c.adw_tab_overview_set_open(tab_overview, 1 - c.adw_tab_overview_get_open(tab_overview));
|
|
}
|
|
}
|
|
|
|
/// Toggle the maximized state for this window.
|
|
pub fn toggleMaximize(self: *Window) void {
|
|
if (self.config.maximize) {
|
|
c.gtk_window_unmaximize(self.window);
|
|
} else {
|
|
c.gtk_window_maximize(self.window);
|
|
}
|
|
// We update the config and call syncAppearance
|
|
// in the gtkWindowNotifyMaximized callback
|
|
}
|
|
|
|
/// Toggle fullscreen for this window.
|
|
pub fn toggleFullscreen(self: *Window) void {
|
|
if (self.config.fullscreen) {
|
|
c.gtk_window_unfullscreen(self.window);
|
|
} else {
|
|
c.gtk_window_fullscreen(self.window);
|
|
}
|
|
// We update the config and call syncAppearance
|
|
// in the gtkWindowNotifyFullscreened callback
|
|
}
|
|
|
|
/// Toggle the window decorations for this window.
|
|
pub fn toggleWindowDecorations(self: *Window) void {
|
|
self.config.window_decoration = switch (self.config.window_decoration) {
|
|
.none => switch (self.app.config.@"window-decoration") {
|
|
// If we started as none, then we switch to auto
|
|
.none => .auto,
|
|
// Switch back
|
|
.auto, .client, .server => |v| v,
|
|
},
|
|
// Always set to none
|
|
.auto, .client, .server => .none,
|
|
};
|
|
|
|
self.syncAppearance() catch |err| {
|
|
log.err("failed to sync appearance={}", .{err});
|
|
};
|
|
}
|
|
|
|
/// Grabs focus on the currently selected tab.
|
|
pub fn focusCurrentTab(self: *Window) void {
|
|
const tab = self.notebook.currentTab() orelse return;
|
|
const surface = tab.focus_child orelse return;
|
|
const gl_area = @as(*c.GtkWidget, @ptrCast(surface.gl_area));
|
|
_ = c.gtk_widget_grab_focus(gl_area);
|
|
|
|
if (surface.getTitle()) |title| {
|
|
self.setTitle(title);
|
|
}
|
|
}
|
|
|
|
pub fn onConfigReloaded(self: *Window) void {
|
|
self.sendToast("Reloaded the configuration");
|
|
}
|
|
|
|
pub fn sendToast(self: *Window, title: [:0]const u8) void {
|
|
const toast = c.adw_toast_new(title);
|
|
c.adw_toast_set_timeout(toast, 3);
|
|
c.adw_toast_overlay_add_toast(@ptrCast(self.toast_overlay), toast);
|
|
}
|
|
|
|
fn gtkRealize(_: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
|
const self = userdataSelf(ud.?);
|
|
|
|
// Initialize our window protocol logic
|
|
if (winproto.Window.init(
|
|
self.app.core_app.alloc,
|
|
&self.app.winproto,
|
|
self,
|
|
)) |wp| {
|
|
self.winproto = wp;
|
|
} else |err| {
|
|
log.warn("failed to initialize window protocol error={}", .{err});
|
|
}
|
|
|
|
// When we are realized we always setup our appearance
|
|
self.syncAppearance() catch |err| {
|
|
log.err("failed to initialize appearance={}", .{err});
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
fn gtkWindowNotifyMaximized(
|
|
_: *c.GObject,
|
|
_: *c.GParamSpec,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
const self = userdataSelf(ud orelse return);
|
|
self.config.maximize = c.gtk_window_is_maximized(self.window) != 0;
|
|
self.syncAppearance() catch |err| {
|
|
log.err("failed to sync appearance={}", .{err});
|
|
};
|
|
}
|
|
|
|
fn gtkWindowNotifyFullscreened(
|
|
_: *c.GObject,
|
|
_: *c.GParamSpec,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
const self = userdataSelf(ud orelse return);
|
|
self.config.fullscreen = c.gtk_window_is_fullscreen(self.window) != 0;
|
|
self.syncAppearance() catch |err| {
|
|
log.err("failed to sync appearance={}", .{err});
|
|
};
|
|
}
|
|
|
|
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
|
// sends an undefined value.
|
|
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
|
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .new_tab = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
|
|
/// because we need to return an AdwTabPage from this function.
|
|
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
|
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
|
const self: *Window = userdataSelf(ud.?);
|
|
|
|
const alloc = self.app.core_app.alloc;
|
|
const surface = self.actionSurface();
|
|
const tab = Tab.create(alloc, self, surface) catch return null;
|
|
return c.adw_tab_view_get_page(@ptrCast(@alignCast(self.notebook.tab_view)), @ptrCast(@alignCast(tab.box)));
|
|
}
|
|
|
|
fn adwTabOverviewOpen(
|
|
object: *c.GObject,
|
|
_: *c.GParamSpec,
|
|
ud: ?*anyopaque,
|
|
) void {
|
|
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(object));
|
|
|
|
// We only care about when the tab overview is closed.
|
|
if (c.adw_tab_overview_get_open(tab_overview) == 1) {
|
|
return;
|
|
}
|
|
|
|
// On tab overview close, focus is sometimes lost. This is an
|
|
// upstream issue in libadwaita[1]. When this is resolved we
|
|
// can put a runtime version check here to avoid this workaround.
|
|
//
|
|
// Our workaround is to start a timer after 500ms to refocus
|
|
// the currently selected tab. We choose 500ms because the adw
|
|
// animation is 400ms.
|
|
//
|
|
// [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670
|
|
const window: *Window = @ptrCast(@alignCast(ud.?));
|
|
|
|
// If we have an old timer remove it
|
|
if (window.adw_tab_overview_focus_timer) |timer| {
|
|
_ = c.g_source_remove(timer);
|
|
}
|
|
|
|
// Restart our timer
|
|
window.adw_tab_overview_focus_timer = c.g_timeout_add(
|
|
500,
|
|
@ptrCast(&adwTabOverviewFocusTimer),
|
|
window,
|
|
);
|
|
}
|
|
|
|
fn adwTabOverviewFocusTimer(
|
|
self: *Window,
|
|
) callconv(.C) c.gboolean {
|
|
self.adw_tab_overview_focus_timer = null;
|
|
self.focusCurrentTab();
|
|
|
|
// Remove the timer
|
|
return 0;
|
|
}
|
|
|
|
pub fn close(self: *Window) void {
|
|
const window: *gtk.Window = @ptrCast(self.window);
|
|
|
|
// Unset the quick terminal on the app level
|
|
if (self.app.quick_terminal == self) self.app.quick_terminal = null;
|
|
|
|
window.destroy();
|
|
}
|
|
|
|
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
|
_ = v;
|
|
log.debug("window close request", .{});
|
|
const self = userdataSelf(ud.?);
|
|
|
|
// This path should never occur, but this is here as a safety measure.
|
|
if (self.app.quick_terminal == self) return true;
|
|
|
|
// If none of our surfaces need confirmation, we can just exit.
|
|
for (self.app.core_app.surfaces.items) |surface| {
|
|
if (surface.container.window()) |window| {
|
|
if (window == self and
|
|
surface.core_surface.needsConfirmQuit()) break;
|
|
}
|
|
} else {
|
|
self.close();
|
|
return true;
|
|
}
|
|
|
|
// Setup our basic message
|
|
const alert = c.gtk_message_dialog_new(
|
|
self.window,
|
|
c.GTK_DIALOG_MODAL,
|
|
c.GTK_MESSAGE_QUESTION,
|
|
c.GTK_BUTTONS_YES_NO,
|
|
"Close this window?",
|
|
);
|
|
c.gtk_message_dialog_format_secondary_text(
|
|
@ptrCast(alert),
|
|
"All terminal sessions in this window will be terminated.",
|
|
);
|
|
|
|
// We want the "yes" to appear destructive.
|
|
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
|
@ptrCast(alert),
|
|
c.GTK_RESPONSE_YES,
|
|
);
|
|
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
|
|
|
|
// We want the "no" to be the default action
|
|
c.gtk_dialog_set_default_response(
|
|
@ptrCast(alert),
|
|
c.GTK_RESPONSE_NO,
|
|
);
|
|
|
|
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kCloseConfirmation), self, null, c.G_CONNECT_DEFAULT);
|
|
|
|
c.gtk_widget_show(alert);
|
|
return true;
|
|
}
|
|
|
|
fn gtkCloseConfirmation(
|
|
alert: *c.GtkMessageDialog,
|
|
response: c.gint,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
c.gtk_window_destroy(@ptrCast(alert));
|
|
if (response == c.GTK_RESPONSE_YES) {
|
|
const self = userdataSelf(ud.?);
|
|
self.close();
|
|
}
|
|
}
|
|
|
|
/// "destroy" signal for the window
|
|
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|
_ = v;
|
|
log.debug("window destroy", .{});
|
|
|
|
const self = userdataSelf(ud.?);
|
|
const alloc = self.app.core_app.alloc;
|
|
self.deinit();
|
|
alloc.destroy(self);
|
|
}
|
|
|
|
fn gtkKeyPressed(
|
|
ec_key: *c.GtkEventControllerKey,
|
|
keyval: c.guint,
|
|
keycode: c.guint,
|
|
gtk_mods: c.GdkModifierType,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) c.gboolean {
|
|
const self = userdataSelf(ud.?);
|
|
|
|
// We only process window-level events currently for the tab
|
|
// overview. This is primarily defensive programming because
|
|
// I'm not 100% certain how our logic below will interact with
|
|
// other parts of the application but I know for sure we must
|
|
// handle this during the tab overview.
|
|
//
|
|
// If someone can confidently show or explain that this is not
|
|
// necessary, please remove this check.
|
|
if (adwaita.versionAtLeast(1, 4, 0)) {
|
|
if (self.tab_overview) |tab_overview_widget| {
|
|
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
|
|
if (c.adw_tab_overview_get_open(tab_overview) == 0) return 0;
|
|
}
|
|
}
|
|
|
|
const surface = self.app.core_app.focusedSurface() orelse return 0;
|
|
return if (surface.rt_surface.keyEvent(
|
|
.press,
|
|
ec_key,
|
|
keyval,
|
|
keycode,
|
|
gtk_mods,
|
|
)) 1 else 0;
|
|
}
|
|
|
|
fn gtkActionAbout(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const name = "Ghostty";
|
|
const icon = "com.mitchellh.ghostty";
|
|
const website = "https://ghostty.org";
|
|
|
|
if (adwaita.versionAtLeast(1, 5, 0)) {
|
|
c.adw_show_about_dialog(
|
|
@ptrCast(self.window),
|
|
"application-name",
|
|
name,
|
|
"developer-name",
|
|
"Ghostty Developers",
|
|
"application-icon",
|
|
icon,
|
|
"version",
|
|
build_config.version_string.ptr,
|
|
"issue-url",
|
|
"https://github.com/ghostty-org/ghostty/issues",
|
|
"website",
|
|
website,
|
|
@as(?*anyopaque, null),
|
|
);
|
|
} else {
|
|
c.gtk_show_about_dialog(
|
|
self.window,
|
|
"program-name",
|
|
name,
|
|
"logo-icon-name",
|
|
icon,
|
|
"title",
|
|
"About Ghostty",
|
|
"version",
|
|
build_config.version_string.ptr,
|
|
"website",
|
|
website,
|
|
@as(?*anyopaque, null),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn gtkActionClose(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
self.close();
|
|
}
|
|
|
|
fn gtkActionNewWindow(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .new_window = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionNewTab(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
// We can use undefined because the button is not used.
|
|
gtkTabNewClick(undefined, self);
|
|
}
|
|
|
|
fn gtkActionCloseTab(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionSplitRight(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .new_split = .right }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionSplitDown(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .new_split = .down }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionSplitLeft(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .new_split = .left }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionSplitUp(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .new_split = .up }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionToggleInspector(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .inspector = .toggle }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionCopy(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .copy_to_clipboard = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionPaste(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .paste_from_clipboard = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionReset(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .reset = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionClear(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn gtkActionPromptTitle(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.C) void {
|
|
const surface = self.actionSurface() orelse return;
|
|
_ = surface.performBindingAction(.{ .prompt_surface_title = {} }) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
/// Returns the surface to use for an action.
|
|
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;
|
|
}
|
|
|
|
fn gtkTitlebarMenuActivate(
|
|
btn: *c.GtkMenuButton,
|
|
_: *c.GParamSpec,
|
|
ud: ?*anyopaque,
|
|
) callconv(.C) void {
|
|
// debian 12 is stuck on GTK 4.8
|
|
if (!version.atLeast(4, 10, 0)) return;
|
|
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();
|
|
}
|
|
}
|
|
|
|
fn userdataSelf(ud: *anyopaque) *Window {
|
|
return @ptrCast(@alignCast(ud));
|
|
}
|