gtk: require libadwaita

This commit removes support for building without libadwaita.
This commit is contained in:
Jeffrey C. Ollie
2025-01-30 22:59:36 -06:00
parent b975f1e860
commit 25c5ecf553
16 changed files with 416 additions and 1170 deletions

View File

@ -374,7 +374,7 @@ jobs:
run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs
run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs
- name: Test GLFW Build
run: nix develop -c zig build -Dapp-runtime=glfw
@ -387,10 +387,9 @@ jobs:
strategy:
fail-fast: false
matrix:
adwaita: ["true", "false"]
x11: ["true", "false"]
wayland: ["true", "false"]
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
@ -421,7 +420,6 @@ jobs:
nix develop -c \
zig build \
-Dapp-runtime=gtk \
-Dgtk-adwaita=${{ matrix.adwaita }} \
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }}

View File

@ -25,7 +25,6 @@ const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig");
const adwaita = @import("adwaita.zig");
const cgroup = @import("cgroup.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
@ -109,6 +108,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
c.gtk_get_micro_version(),
});
// log the adwaita version
log.info("libadwaita version build={s} runtime={}.{}.{}", .{
c.ADW_VERSION_S,
c.adw_get_major_version(),
c.adw_get_minor_version(),
c.adw_get_micro_version(),
});
// Load our configuration
var config = try Config.load(core_app.alloc);
errdefer config.deinit();
@ -236,7 +243,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}
}
c.gtk_init();
c.adw_init();
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
// I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here.
@ -244,16 +252,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
std.posix.exit(1);
};
// If we're using libadwaita, log the version
if (adwaita.enabled(&config)) {
log.info("libadwaita version build={s} runtime={}.{}.{}", .{
c.ADW_VERSION_S,
c.adw_get_major_version(),
c.adw_get_minor_version(),
c.adw_get_micro_version(),
});
}
// The "none" cursor is used for hiding the cursor
const cursor_none = c.gdk_cursor_new_from_name("none", null);
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
@ -288,103 +286,38 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
};
// Create our GTK Application which encapsulates our process.
const app: *c.GtkApplication = app: {
log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
app_id,
single_instance,
adwaita,
});
log.debug("creating GTK application id={s} single-instance={}", .{
app_id,
single_instance,
});
// If not libadwaita, create a standard GTK application.
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
!adwaita.enabled(&config))
{
{
const provider = c.gtk_css_provider_new();
defer c.g_object_unref(provider);
switch (config.@"window-theme") {
.system, .light => {},
.dark => {
const settings = c.gtk_settings_get_default();
c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
// Using an AdwApplication lets us use Adwaita widgets and access things
// such as the color scheme.
const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
app_id.ptr,
app_flags,
))) orelse return error.GtkInitFailed;
errdefer c.g_object_unref(adw_app);
c.gtk_css_provider_load_from_resource(
provider,
"/com/mitchellh/ghostty/style-dark.css",
);
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
);
},
.auto, .ghostty => {
const lum = config.background.toTerminalRGB().perceivedLuminance();
if (lum <= 0.5) {
const settings = c.gtk_settings_get_default();
c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
c.gtk_css_provider_load_from_resource(
provider,
"/com/mitchellh/ghostty/style-dark.css",
);
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
);
}
},
}
}
{
const provider = c.gtk_css_provider_new();
defer c.g_object_unref(provider);
c.gtk_css_provider_load_from_resource(provider, "/com/mitchellh/ghostty/style.css");
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
);
}
break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
app_id.ptr,
app_flags,
))) orelse return error.GtkInitFailed;
}
// Use libadwaita if requested. Using an AdwApplication lets us use
// Adwaita widgets and access things such as the color scheme.
const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
app_id.ptr,
app_flags,
))) orelse return error.GtkInitFailed;
const style_manager = c.adw_application_get_style_manager(adw_app);
c.adw_style_manager_set_color_scheme(
style_manager,
switch (config.@"window-theme") {
.auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5)
c.ADW_COLOR_SCHEME_PREFER_LIGHT
else
c.ADW_COLOR_SCHEME_PREFER_DARK;
},
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
const style_manager = c.adw_application_get_style_manager(adw_app);
c.adw_style_manager_set_color_scheme(
style_manager,
switch (config.@"window-theme") {
.auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5)
c.ADW_COLOR_SCHEME_PREFER_LIGHT
else
c.ADW_COLOR_SCHEME_PREFER_DARK;
},
);
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
},
);
break :app @ptrCast(adw_app);
};
errdefer c.g_object_unref(app);
const gapp = @as(*c.GApplication, @ptrCast(app));
const app: *c.GtkApplication = @ptrCast(adw_app);
const gapp: *c.GApplication = @ptrCast(app);
// force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources
@ -980,11 +913,9 @@ fn configChange(
// App changes needs to show a toast that our configuration
// has reloaded.
if (adwaita.enabled(&self.config)) {
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
},
}

View File

@ -22,8 +22,8 @@ const Tab = @import("Tab.zig");
const c = @import("c.zig").c;
const adwaita = @import("adwaita.zig");
const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar;
const Notebook = @import("notebook.zig");
const HeaderBar = @import("headerbar.zig");
const version = @import("version.zig");
const winproto = @import("winproto.zig");
@ -34,9 +34,7 @@ app: *App,
/// Our window
window: *c.GtkWindow,
/// The header bar for the window. This is possibly null since it can be
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
/// GtkHeaderBar depending on if adw is enabled and linked.
/// The header bar for the window.
headerbar: HeaderBar,
/// The tab overview for the window. This is possibly null since there is no
@ -44,14 +42,12 @@ headerbar: HeaderBar,
tab_overview: ?*c.GtkWidget,
/// The notebook (tab grouping) for this window.
/// can be either c.GtkNotebook or c.AdwTabView.
notebook: Notebook,
context_menu: *c.GtkWidget,
/// The libadwaita widget for receiving toast send requests. If libadwaita is
/// not used, this is null and unused.
toast_overlay: ?*c.GtkWidget,
/// 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,
@ -87,37 +83,27 @@ pub fn init(self: *Window, app: *App) !void {
};
// Create the window
const window: *c.GtkWidget = window: {
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) {
const window = c.adw_application_window_new(app.app);
c.gtk_widget_add_css_class(@ptrCast(window), "adw");
break :window window;
} else {
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));
const gtk_widget = c.adw_application_window_new(app.app);
errdefer c.gtk_window_destroy(@ptrCast(gtk_widget));
const gtk_window: *c.GtkWindow = @ptrCast(window);
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty");
c.gtk_window_set_default_size(gtk_window, 1000, 600);
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window");
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "terminal-window");
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(gtk_window, 0);
c.gtk_window_set_handle_menubar_accel(self.window, 0);
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
c.gtk_window_set_icon_name(self.window, build_config.bundle_id);
// 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.
if (!version.atLeast(4, 16, 0) and app.config.@"window-theme" == .ghostty) {
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
c.gtk_widget_add_css_class(gtk_widget, "window-theme-ghostty");
}
// Create our box which will hold our widgets in the main content area.
@ -127,9 +113,9 @@ pub fn init(self: *Window, app: *App) !void {
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: {
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), self.notebook.adw.tab_view);
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.tab_view);
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
_ = c.g_signal_connect_data(
tab_overview,
@ -166,10 +152,9 @@ pub fn init(self: *Window, app: *App) !void {
// 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));
assert(adwaita.versionAtLeast(1, 4, 0));
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
.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");
@ -186,7 +171,7 @@ pub fn init(self: *Window, app: *App) !void {
.hidden => btn: {
const btn = c.adw_tab_button_new();
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.tab_view);
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
break :btn btn;
},
@ -203,13 +188,13 @@ pub fn init(self: *Window, app: *App) !void {
self.headerbar.packStart(btn);
}
_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(&gtkWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(&gtkWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(&gtkWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::decorated", c.G_CALLBACK(&gtkWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::maximized", c.G_CALLBACK(&gtkWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::fullscreened", c.G_CALLBACK(&gtkWindowNotifyFullscreened), 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)) {
if (!adwaita.versionAtLeast(1, 4, 0)) {
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
}
@ -218,10 +203,7 @@ 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))
{
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));
@ -231,30 +213,22 @@ pub fn init(self: *Window, app: *App) !void {
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(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 = 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 = 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 (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);
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.tab_view);
}
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
@ -273,40 +247,39 @@ pub fn init(self: *Window, app: *App) !void {
// 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(window, ec_key_press);
c.gtk_widget_add_controller(gtk_widget, ec_key_press);
// All of our events
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(&gtkRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(&gtkRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(&gtkKeyPressed), self, null, c.G_CONNECT_DEFAULT);
// Our actions for the menu
initActions(self);
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
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.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_view(tab_bar, self.notebook.tab_view);
if (!app.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.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),
.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.app.config.@"adw-toolbar-style") {
const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"gtk-toolbar-style") {
.flat => c.ADW_TOOLBAR_FLAT,
.raised => c.ADW_TOOLBAR_RAISED,
.@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER,
@ -320,51 +293,34 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(@alignCast(toolbar_view)),
);
c.adw_application_window_set_content(
@ptrCast(gtk_window),
@ptrCast(gtk_widget),
@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)),
),
.hidden => unreachable,
}
c.adw_tab_bar_set_view(tab_bar, adw.tab_view);
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
},
.gtk => {},
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 => 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.tab_view);
// 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());
c.gtk_window_set_child(gtk_window, box);
}
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
}
// Show the window
c.gtk_widget_show(window);
c.gtk_widget_show(gtk_widget);
}
pub fn updateConfig(
@ -407,19 +363,16 @@ 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),
);
}
if (self.tab_overview) |tab_overview| {
assert(adwaita.versionAtLeast(1, 4, 0));
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),
);
}
}
@ -556,7 +509,7 @@ pub fn gotoTab(self: *Window, n: usize) bool {
/// Toggle tab overview (if present)
pub fn toggleTabOverview(self: *Window) void {
if (self.tab_overview) |tab_overview_widget| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(adwaita.versionAtLeast(1, 4, 0));
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));
}
@ -603,11 +556,9 @@ pub fn onConfigReloaded(self: *Window) void {
}
pub fn sendToast(self: *Window, title: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) return;
const toast_overlay = self.toast_overlay orelse return;
const toast = c.adw_toast_new(title);
c.adw_toast_set_timeout(toast, 3);
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
c.adw_toast_overlay_add_toast(@ptrCast(self.toast_overlay), toast);
}
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
@ -711,12 +662,12 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
/// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
const self: *Window = userdataSelf(ud.?);
assert((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config));
assert(adwaita.versionAtLeast(1, 4, 0));
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(self.notebook.adw.tab_view, @ptrCast(@alignCast(tab.box)));
return c.adw_tab_view_get_page(self.notebook.tab_view, @ptrCast(@alignCast(tab.box)));
}
fn adwTabOverviewOpen(
@ -863,7 +814,7 @@ fn gtkKeyPressed(
//
// If someone can confidently show or explain that this is not
// necessary, please remove this check.
if (comptime adwaita.versionAtLeast(1, 4, 0)) {
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;
@ -891,10 +842,7 @@ fn gtkActionAbout(
const icon = "com.mitchellh.ghostty";
const website = "https://ghostty.org";
if ((comptime adwaita.versionAtLeast(1, 5, 0)) and
adwaita.versionAtLeast(1, 5, 0) and
adwaita.enabled(&self.app.config))
{
if (adwaita.versionAtLeast(1, 5, 0)) {
c.adw_show_about_dialog(
@ptrCast(self.window),
"application-name",

View File

@ -1,20 +1,5 @@
const std = @import("std");
const c = @import("c.zig").c;
const build_options = @import("build_options");
const Config = @import("../../config.zig").Config;
/// Returns true if Ghostty is configured to build with libadwaita and
/// the configuration has enabled adwaita.
///
/// For a comptime version of this function, use `versionAtLeast` in
/// a comptime context with all the version numbers set to 0.
///
/// This must be `inline` so that the comptime check noops conditional
/// paths that are not enabled.
pub inline fn enabled(config: *const Config) bool {
return build_options.adwaita and
config.@"gtk-adwaita";
}
/// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to
@ -33,8 +18,6 @@ pub inline fn versionAtLeast(
comptime minor: u16,
comptime micro: u16,
) bool {
if (comptime !build_options.adwaita) return false;
// If our header has lower versions than the given version,
// we can return false immediately. This prevents us from
// compiling against unknown symbols and makes runtime checks

View File

@ -2,7 +2,7 @@ const std = @import("std");
const build_options = @import("build_options");
const gtk = @import("gtk");
const adw = if (build_options.adwaita) @import("adw") else void;
const adw = @import("adw");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@ -20,19 +20,12 @@ pub fn main() !void {
const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0);
defer alloc.free(data);
if ((comptime !build_options.adwaita) and std.mem.indexOf(u8, data, "lib=\"Adw\"") != null) {
std.debug.print("{s}: skipping builder check because Adwaita is not enabled!\n", .{filename});
return;
}
if (gtk.initCheck() == 0) {
std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
return;
}
if (comptime build_options.adwaita) {
adw.init();
}
adw.init();
const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
defer builder.unref();

View File

@ -3,9 +3,7 @@ const build_options = @import("build_options");
/// Imported C API directly from header files
pub const c = @cImport({
@cInclude("gtk/gtk.h");
if (build_options.adwaita) {
@cInclude("adwaita.h");
}
@cInclude("adwaita.h");
if (build_options.x11) {
// Add in X11-specific GDK backend which we use for specific things

View File

@ -1,58 +1,59 @@
const HeaderBar = @This();
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBarAdw = @import("headerbar_adw.zig");
const HeaderBarGtk = @import("headerbar_gtk.zig");
/// the Adwaita headerbar widget
headerbar: *c.AdwHeaderBar,
pub const HeaderBar = union(enum) {
adw: HeaderBarAdw,
gtk: HeaderBarGtk,
/// the Adwaita window title widget
title: *c.AdwWindowTitle,
pub fn init(self: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", self);
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) {
HeaderBarAdw.init(self);
} else {
HeaderBarGtk.init(self);
}
}
pub fn init(self: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", self);
self.* = .{
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
.title = @ptrCast(@alignCast(c.adw_window_title_new(
c.gtk_window_get_title(window.window) orelse "Ghostty",
null,
))),
};
c.adw_header_bar_set_title_widget(
self.headerbar,
@ptrCast(@alignCast(self.title)),
);
}
pub fn setVisible(self: HeaderBar, visible: bool) void {
switch (self) {
inline else => |v| v.setVisible(visible),
}
}
pub fn setVisible(self: *const HeaderBar, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
inline else => |v| v.asWidget(),
};
}
pub fn asWidget(self: *const HeaderBar) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}
pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
inline else => |v| v.packEnd(widget),
}
}
pub fn packEnd(self: *const HeaderBar, widget: *c.GtkWidget) void {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
inline else => |v| v.packStart(widget),
}
}
pub fn packStart(self: *const HeaderBar, widget: *c.GtkWidget) void {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn setTitle(self: HeaderBar, title: [:0]const u8) void {
switch (self) {
inline else => |v| v.setTitle(title),
}
}
pub fn setTitle(self: *const HeaderBar, title: [:0]const u8) void {
const window: *const Window = @fieldParentPtr("headerbar", self);
c.gtk_window_set_title(window.window, title);
c.adw_window_title_set_title(self.title, title);
}
pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void {
switch (self) {
inline else => |v| v.setSubtitle(subtitle),
}
}
};
pub fn setSubtitle(self: *const HeaderBar, subtitle: [:0]const u8) void {
c.adw_window_title_set_subtitle(self.title, subtitle);
}

View File

@ -1,78 +0,0 @@
const HeaderBarAdw = @This();
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBar = @import("headerbar.zig").HeaderBar;
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else anyopaque;
const AdwWindowTitle = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwWindowTitle else anyopaque;
/// the window that this headerbar is attached to
window: *Window,
/// the Adwaita headerbar widget
headerbar: *AdwHeaderBar,
/// the Adwaita window title widget
title: *AdwWindowTitle,
pub fn init(headerbar: *HeaderBar) void {
if (!adwaita.versionAtLeast(0, 0, 0)) return;
const window: *Window = @fieldParentPtr("headerbar", headerbar);
headerbar.* = .{
.adw = .{
.window = window,
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
.title = @ptrCast(@alignCast(c.adw_window_title_new(
c.gtk_window_get_title(window.window) orelse "Ghostty",
null,
))),
},
};
c.adw_header_bar_set_title_widget(
headerbar.adw.headerbar,
@ptrCast(@alignCast(headerbar.adw.title)),
);
}
pub fn setVisible(self: HeaderBarAdw, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBarAdw) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}
pub fn packEnd(self: HeaderBarAdw, widget: *c.GtkWidget) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
}
pub fn packStart(self: HeaderBarAdw, widget: *c.GtkWidget) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
}
pub fn setTitle(self: HeaderBarAdw, title: [:0]const u8) void {
c.gtk_window_set_title(self.window.window, title);
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_window_title_set_title(self.title, title);
}
}
pub fn setSubtitle(self: HeaderBarAdw, subtitle: [:0]const u8) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_window_title_set_subtitle(self.title, subtitle);
}
}

View File

@ -1,52 +0,0 @@
const HeaderBarGtk = @This();
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBar = @import("headerbar.zig").HeaderBar;
/// the window that this headarbar is attached to
window: *Window,
/// the GTK headerbar widget
headerbar: *c.GtkHeaderBar,
pub fn init(headerbar: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", headerbar);
headerbar.* = .{
.gtk = .{
.window = window,
.headerbar = @ptrCast(c.gtk_header_bar_new()),
},
};
}
pub fn setVisible(self: HeaderBarGtk, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBarGtk) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}
pub fn packEnd(self: HeaderBarGtk, widget: *c.GtkWidget) void {
c.gtk_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn packStart(self: HeaderBarGtk, widget: *c.GtkWidget) void {
c.gtk_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn setTitle(self: HeaderBarGtk, title: [:0]const u8) void {
c.gtk_window_set_title(self.window.window, title);
}
pub fn setSubtitle(_: HeaderBarGtk, _: [:0]const u8) void {}

View File

@ -1,169 +1,193 @@
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
const Notebook = @This();
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const NotebookAdw = @import("notebook_adw.zig").NotebookAdw;
const NotebookGtk = @import("notebook_gtk.zig").NotebookGtk;
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
/// the tab view
tab_view: *c.AdwTabView,
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
pub const Notebook = union(enum) {
adw: NotebookAdw,
gtk: NotebookGtk,
/// Set to true so that the adw close-page handler knows we're forcing
/// and to allow a close to happen with no confirm. This is a bit of a hack
/// because we currently use GTK alerts to confirm tab close and they
/// don't carry with them the ADW state that we are confirming or not.
/// Long term we should move to ADW alerts so we can know if we are
/// confirming or not.
forcing_close: bool = false,
pub fn init(self: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", self);
const app = window.app;
if (adwaita.enabled(&app.config)) return NotebookAdw.init(self);
pub fn init(self: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", self);
return NotebookGtk.init(self);
const tab_view: *c.AdwTabView = c.adw_tab_view_new() orelse unreachable;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
if (adwaita.versionAtLeast(1, 2, 0)) {
// Adwaita enables all of the shortcuts by default.
// We want to manage keybindings ourselves.
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
}
pub fn asWidget(self: *Notebook) *c.GtkWidget {
return switch (self.*) {
.adw => |*adw| adw.asWidget(),
.gtk => |*gtk| gtk.asWidget(),
};
self.* = .{
.tab_view = tab_view,
};
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
}
pub fn asWidget(self: *Notebook) *c.GtkWidget {
return @ptrCast(@alignCast(self.tab_view));
}
pub fn nPages(self: *Notebook) c_int {
return c.adw_tab_view_get_n_pages(self.tab_view);
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
fn currentPage(self: *Notebook) ?c_int {
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
return c.adw_tab_view_get_page_position(self.tab_view, page);
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *Notebook) ?*Tab {
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
const child = c.adw_tab_page_get_child(page);
return @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
));
}
pub fn gotoNthTab(self: *Notebook, position: c_int) bool {
const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position) orelse return false;
c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
return true;
}
pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
return c.adw_tab_view_get_page_position(self.tab_view, page);
}
pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
// The next index is the previous or we wrap around.
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
const max = self.nPages();
break :next_idx max -| 1;
};
// Do nothing if we have one tab
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn gotoNextTab(self: *Notebook, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
const max = self.nPages() -| 1;
const next_idx = if (page_idx < max) page_idx + 1 else 0;
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
const page_idx = self.getTabPosition(tab) orelse return;
const max = self.nPages() -| 1;
var new_position: c_int = page_idx + position;
if (new_position < 0) {
new_position = max + new_position + 1;
} else if (new_position > max) {
new_position = new_position - max - 1;
}
pub fn nPages(self: *Notebook) c_int {
return switch (self.*) {
.adw => |*adw| adw.nPages(),
.gtk => |*gtk| gtk.nPages(),
};
if (new_position == page_idx) return;
self.reorderPage(tab, new_position);
}
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
_ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
}
pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_title(page, title.ptr);
}
pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
}
fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
const numPages = self.nPages();
return switch (tab.window.app.config.@"window-new-tab-position") {
.current => if (self.currentPage()) |page| page + 1 else numPages,
.end => numPages,
};
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
const position = self.newTabInsertPosition(tab);
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
c.adw_tab_page_set_title(page, title.ptr);
c.adw_tab_view_set_selected_page(self.tab_view, page);
}
pub fn closeTab(self: *Notebook, tab: *Tab) void {
// closeTab always expects to close unconditionally so we mark this
// as true so that the close_page call below doesn't request
// confirmation.
self.forcing_close = true;
const n = self.nPages();
defer {
// self becomes invalid if we close the last page because we close
// the whole window
if (n > 1) self.forcing_close = false;
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
fn currentPage(self: *Notebook) ?c_int {
return switch (self.*) {
.adw => |*adw| adw.currentPage(),
.gtk => |*gtk| gtk.currentPage(),
};
}
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
c.adw_tab_view_close_page(self.tab_view, page);
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *Notebook) ?*Tab {
return switch (self.*) {
.adw => |*adw| adw.currentTab(),
.gtk => |*gtk| gtk.currentTab(),
};
}
// If we have no more tabs we close the window
if (self.nPages() == 0) {
const window = tab.window.window;
pub fn gotoNthTab(self: *Notebook, position: c_int) bool {
const current_page_ = self.currentPage();
if (current_page_) |current_page| if (current_page == position) return false;
switch (self.*) {
.adw => |*adw| adw.gotoNthTab(position),
.gtk => |*gtk| gtk.gotoNthTab(position),
}
return true;
}
pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
return switch (self.*) {
.adw => |*adw| adw.getTabPosition(tab),
.gtk => |*gtk| gtk.getTabPosition(tab),
};
}
pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
// The next index is the previous or we wrap around.
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
const max = self.nPages();
break :next_idx max -| 1;
};
// Do nothing if we have one tab
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn gotoNextTab(self: *Notebook, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
const max = self.nPages() -| 1;
const next_idx = if (page_idx < max) page_idx + 1 else 0;
// Do nothing if we have one tab
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
const page_idx = self.getTabPosition(tab) orelse return;
const max = self.nPages() -| 1;
var new_position: c_int = page_idx + position;
if (new_position < 0) {
new_position = max + new_position + 1;
} else if (new_position > max) {
new_position = new_position - max - 1;
// libadw versions <= 1.3.x leak the final page view
// which causes our surface to not properly cleanup. We
// unref to force the cleanup. This will trigger a critical
// warning from GTK, but I don't know any other workaround.
// Note: I'm not actually sure if 1.4.0 contains the fix,
// I just know that 1.3.x is broken and 1.5.1 is fixed.
// If we know that 1.4.0 is fixed, we can change this.
if (!adwaita.versionAtLeast(1, 4, 0)) {
c.g_object_unref(tab.box);
}
if (new_position == page_idx) return;
self.reorderPage(tab, new_position);
// `self` will become invalid after this call because it will have
// been freed up as part of the process of closing the window.
c.gtk_window_destroy(window);
}
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
switch (self.*) {
.adw => |*adw| adw.reorderPage(tab, position),
.gtk => |*gtk| gtk.reorderPage(tab, position),
}
}
pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
switch (self.*) {
.adw => |*adw| adw.setTabLabel(tab, title),
.gtk => |*gtk| gtk.setTabLabel(tab, title),
}
}
pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
switch (self.*) {
.adw => |*adw| adw.setTabTooltip(tab, tooltip),
.gtk => |*gtk| gtk.setTabTooltip(tab, tooltip),
}
}
fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
const numPages = self.nPages();
return switch (tab.window.app.config.@"window-new-tab-position") {
.current => if (self.currentPage()) |page| page + 1 else numPages,
.end => numPages,
};
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
const position = self.newTabInsertPosition(tab);
switch (self.*) {
.adw => |*adw| adw.addTab(tab, position, title),
.gtk => |*gtk| gtk.addTab(tab, position, title),
}
}
pub fn closeTab(self: *Notebook, tab: *Tab) void {
switch (self.*) {
.adw => |*adw| adw.closeTab(tab),
.gtk => |*gtk| gtk.closeTab(tab),
}
}
};
}
pub fn createWindow(currentWindow: *Window) !*Window {
const alloc = currentWindow.app.core_app.alloc;
@ -172,3 +196,54 @@ pub fn createWindow(currentWindow: *Window) !*Window {
// Create a new window
return Window.create(alloc, app);
}
fn adwPageAttached(_: *c.AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
tab.window = window;
window.focusCurrentTab();
}
fn adwClosePage(
_: *c.AdwTabView,
page: *c.AdwTabPage,
ud: ?*anyopaque,
) callconv(.C) c.gboolean {
const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
@ptrCast(child),
Tab.GHOSTTY_TAB,
) orelse return 0));
const window: *Window = @ptrCast(@alignCast(ud.?));
const notebook = window.notebook;
c.adw_tab_view_close_page_finish(
notebook.tab_view,
page,
@intFromBool(notebook.forcing_close),
);
if (!notebook.forcing_close) tab.closeWithConfirmation();
return 1;
}
fn adwTabViewCreateWindow(
_: *c.AdwTabView,
ud: ?*anyopaque,
) callconv(.C) ?*c.AdwTabView {
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
const window = createWindow(currentWindow) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
return window.notebook.tab_view;
}
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(window.notebook.tab_view) orelse return;
const title = c.adw_tab_page_get_title(page);
window.setTitle(std.mem.span(title));
}

View File

@ -1,209 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const Notebook = @import("notebook.zig").Notebook;
const createWindow = @import("notebook.zig").createWindow;
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
const AdwTabPage = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabPage else anyopaque;
pub const NotebookAdw = struct {
/// the tab view
tab_view: *AdwTabView,
/// Set to true so that the adw close-page handler knows we're forcing
/// and to allow a close to happen with no confirm. This is a bit of a hack
/// because we currently use GTK alerts to confirm tab close and they
/// don't carry with them the ADW state that we are confirming or not.
/// Long term we should move to ADW alerts so we can know if we are
/// confirming or not.
forcing_close: bool = false,
pub fn init(notebook: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", notebook);
const app = window.app;
assert(adwaita.enabled(&app.config));
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
// Adwaita enables all of the shortcuts by default.
// We want to manage keybindings ourselves.
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
}
notebook.* = .{
.adw = .{
.tab_view = tab_view,
},
};
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
}
pub fn asWidget(self: *NotebookAdw) *c.GtkWidget {
return @ptrCast(@alignCast(self.tab_view));
}
pub fn nPages(self: *NotebookAdw) c_int {
if (comptime adwaita.versionAtLeast(0, 0, 0))
return c.adw_tab_view_get_n_pages(self.tab_view)
else
unreachable;
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
pub fn currentPage(self: *NotebookAdw) ?c_int {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
return c.adw_tab_view_get_page_position(self.tab_view, page);
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *NotebookAdw) ?*Tab {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
const child = c.adw_tab_page_get_child(page);
return @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
));
}
pub fn gotoNthTab(self: *NotebookAdw, position: c_int) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position);
c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
}
pub fn getTabPosition(self: *NotebookAdw, tab: *Tab) ?c_int {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
return c.adw_tab_view_get_page_position(self.tab_view, page);
}
pub fn reorderPage(self: *NotebookAdw, tab: *Tab, position: c_int) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
_ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
}
pub fn setTabLabel(self: *NotebookAdw, tab: *Tab, title: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_title(page, title.ptr);
}
pub fn setTabTooltip(self: *NotebookAdw, tab: *Tab, tooltip: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
}
pub fn addTab(self: *NotebookAdw, tab: *Tab, position: c_int, title: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
c.adw_tab_page_set_title(page, title.ptr);
c.adw_tab_view_set_selected_page(self.tab_view, page);
}
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
// closeTab always expects to close unconditionally so we mark this
// as true so that the close_page call below doesn't request
// confirmation.
self.forcing_close = true;
const n = self.nPages();
defer {
// self becomes invalid if we close the last page because we close
// the whole window
if (n > 1) self.forcing_close = false;
}
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
c.adw_tab_view_close_page(self.tab_view, page);
// If we have no more tabs we close the window
if (self.nPages() == 0) {
const window = tab.window.window;
// libadw versions <= 1.3.x leak the final page view
// which causes our surface to not properly cleanup. We
// unref to force the cleanup. This will trigger a critical
// warning from GTK, but I don't know any other workaround.
// Note: I'm not actually sure if 1.4.0 contains the fix,
// I just know that 1.3.x is broken and 1.5.1 is fixed.
// If we know that 1.4.0 is fixed, we can change this.
if (!adwaita.versionAtLeast(1, 4, 0)) {
c.g_object_unref(tab.box);
}
// `self` will become invalid after this call because it will have
// been freed up as part of the process of closing the window.
c.gtk_window_destroy(window);
}
}
};
fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
tab.window = window;
window.focusCurrentTab();
}
fn adwClosePage(
_: *AdwTabView,
page: *c.AdwTabPage,
ud: ?*anyopaque,
) callconv(.C) c.gboolean {
const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
@ptrCast(child),
Tab.GHOSTTY_TAB,
) orelse return 0));
const window: *Window = @ptrCast(@alignCast(ud.?));
const notebook = window.notebook.adw;
c.adw_tab_view_close_page_finish(
notebook.tab_view,
page,
@intFromBool(notebook.forcing_close),
);
if (!notebook.forcing_close) tab.closeWithConfirmation();
return 1;
}
fn adwTabViewCreateWindow(
_: *AdwTabView,
ud: ?*anyopaque,
) callconv(.C) ?*AdwTabView {
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
const window = createWindow(currentWindow) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
return window.notebook.adw.tab_view;
}
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return;
const title = c.adw_tab_page_get_title(page);
window.setTitle(std.mem.span(title));
}

View File

@ -1,304 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const Notebook = @import("notebook.zig").Notebook;
const createWindow = @import("notebook.zig").createWindow;
const log = std.log.scoped(.gtk);
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
pub const NotebookGtk = struct {
notebook: *c.GtkNotebook,
pub fn init(notebook: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", notebook);
const app = window.app;
// Create a notebook to hold our tabs.
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
c.gtk_widget_add_css_class(notebook_widget, "notebook");
const gtk_notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
.top, .hidden => c.GTK_POS_TOP,
.bottom => c.GTK_POS_BOTTOM,
.left => c.GTK_POS_LEFT,
.right => c.GTK_POS_RIGHT,
};
c.gtk_notebook_set_tab_pos(gtk_notebook, notebook_tab_pos);
c.gtk_notebook_set_scrollable(gtk_notebook, 1);
c.gtk_notebook_set_show_tabs(gtk_notebook, 0);
c.gtk_notebook_set_show_border(gtk_notebook, 0);
// This enables all Ghostty terminal tabs to be exchanged across windows.
c.gtk_notebook_set_group_name(gtk_notebook, "ghostty-terminal-tabs");
// This is important so the notebook expands to fit available space.
// Otherwise, it will be zero/zero in the box below.
c.gtk_widget_set_vexpand(notebook_widget, 1);
c.gtk_widget_set_hexpand(notebook_widget, 1);
// Remove the background from the stack widget
const stack = c.gtk_widget_get_last_child(notebook_widget);
c.gtk_widget_add_css_class(stack, "transparent");
notebook.* = .{
.gtk = .{
.notebook = gtk_notebook,
},
};
// All of our events
_ = c.g_signal_connect_data(gtk_notebook, "page-added", c.G_CALLBACK(&gtkPageAdded), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_notebook, "page-removed", c.G_CALLBACK(&gtkPageRemoved), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_notebook, "create-window", c.G_CALLBACK(&gtkNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
}
/// return the underlying widget as a generic GtkWidget
pub fn asWidget(self: *NotebookGtk) *c.GtkWidget {
return @ptrCast(@alignCast(self.notebook));
}
/// returns the number of pages in the notebook
pub fn nPages(self: *NotebookGtk) c_int {
return c.gtk_notebook_get_n_pages(self.notebook);
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
pub fn currentPage(self: *NotebookGtk) ?c_int {
const current = c.gtk_notebook_get_current_page(self.notebook);
return if (current == -1) null else current;
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *NotebookGtk) ?*Tab {
log.warn("currentTab", .{});
const page = self.currentPage() orelse return null;
const child = c.gtk_notebook_get_nth_page(self.notebook, page);
return @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
));
}
/// focus the nth tab
pub fn gotoNthTab(self: *NotebookGtk, position: c_int) void {
c.gtk_notebook_set_current_page(self.notebook, position);
}
/// get the position of the current tab
pub fn getTabPosition(self: *NotebookGtk, tab: *Tab) ?c_int {
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return null;
return getNotebookPageIndex(page);
}
pub fn reorderPage(self: *NotebookGtk, tab: *Tab, position: c_int) void {
c.gtk_notebook_reorder_child(self.notebook, @ptrCast(tab.box), position);
}
pub fn setTabLabel(_: *NotebookGtk, tab: *Tab, title: [:0]const u8) void {
c.gtk_label_set_text(tab.label_text, title.ptr);
}
pub fn setTabTooltip(_: *NotebookGtk, tab: *Tab, tooltip: [:0]const u8) void {
c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr);
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *NotebookGtk, tab: *Tab, position: c_int, title: [:0]const u8) void {
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
// Build the tab label
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
const label_text_widget = c.gtk_label_new(title.ptr);
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
c.gtk_box_append(label_box, label_text_widget);
tab.label_text = label_text;
const window = tab.window;
if (window.app.config.@"gtk-wide-tabs") {
c.gtk_widget_set_hexpand(label_box_widget, 1);
c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
c.gtk_widget_set_hexpand(label_text_widget, 1);
c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL);
// This ensures that tabs are always equal width. If they're too
// long, they'll be truncated with an ellipsis.
c.gtk_label_set_max_width_chars(label_text, 1);
c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END);
// We need to set a minimum width so that at a certain point
// the notebook will have an arrow button rather than shrinking tabs
// to an unreadably small size.
c.gtk_widget_set_size_request(label_text_widget, 100, 1);
}
// Build the close button for the tab
const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic");
const label_close: *c.GtkButton = @ptrCast(label_close_widget);
c.gtk_button_set_has_frame(label_close, 0);
c.gtk_box_append(label_box, label_close_widget);
const page_idx = c.gtk_notebook_insert_page(
self.notebook,
box_widget,
label_box_widget,
position,
);
// Clicks
const gesture_tab_click = c.gtk_gesture_click_new();
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
// Tab settings
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
c.gtk_notebook_set_tab_detachable(self.notebook, box_widget, 1);
if (self.nPages() > 1) {
c.gtk_notebook_set_show_tabs(self.notebook, 1);
}
// Switch to the new tab
c.gtk_notebook_set_current_page(self.notebook, page_idx);
}
pub fn closeTab(self: *NotebookGtk, tab: *Tab) void {
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
// Find page and tab which we're closing
const page_idx = getNotebookPageIndex(page);
// Remove the page. This will destroy the GTK widgets in the page which
// will trigger Tab cleanup. The `tab` variable is therefore unusable past that point.
c.gtk_notebook_remove_page(self.notebook, page_idx);
const remaining = self.nPages();
switch (remaining) {
// If we have no more tabs we close the window
0 => c.gtk_window_destroy(tab.window.window),
// If we have one more tab we hide the tab bar
1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
else => {},
}
// If we have remaining tabs, we need to make sure we grab focus.
if (remaining > 0)
(self.currentTab() orelse return).window.focusCurrentTab();
}
};
fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
var value: c.GValue = std.mem.zeroes(c.GValue);
defer c.g_value_unset(&value);
_ = c.g_value_init(&value, c.G_TYPE_INT);
c.g_object_get_property(
@ptrCast(@alignCast(page)),
"position",
&value,
);
return c.g_value_get_int(&value);
}
fn gtkPageAdded(
notebook: *c.GtkNotebook,
_: *c.GtkWidget,
page_idx: c.guint,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud.?));
// The added page can come from another window with drag and drop, thus we migrate the tab
// window to be self.
const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx));
const tab: *Tab = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
));
tab.window = self;
// Whenever a new page is added, we always grab focus of the
// currently selected page. This was added specifically so that when
// we drag a tab out to create a new window ("create-window" event)
// we grab focus in the new window. Without this, the terminal didn't
// have focus.
self.focusCurrentTab();
}
fn gtkPageRemoved(
_: *c.GtkNotebook,
_: *c.GtkWidget,
_: c.guint,
ud: ?*anyopaque,
) callconv(.C) void {
log.warn("gtkPageRemoved", .{});
const window: *Window = @ptrCast(@alignCast(ud.?));
// Hide the tab bar if we only have one tab after removal
const remaining = c.gtk_notebook_get_n_pages(window.notebook.gtk.notebook);
if (remaining == 1) {
c.gtk_notebook_set_show_tabs(window.notebook.gtk.notebook, 0);
}
}
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const self = &window.notebook.gtk;
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
const label_text = c.gtk_label_get_text(gtk_label);
window.setTitle(std.mem.span(label_text));
}
fn gtkNotebookCreateWindow(
_: *c.GtkNotebook,
page: *c.GtkWidget,
ud: ?*anyopaque,
) callconv(.C) ?*c.GtkNotebook {
// The tab for the page is stored in the widget data.
const tab: *Tab = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
));
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
const newWindow = createWindow(currentWindow) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
// And add it to the new window.
tab.window = newWindow;
return newWindow.notebook.gtk.notebook;
}
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
const tab: *Tab = @ptrCast(@alignCast(ud));
tab.closeWithConfirmation();
}
fn gtkTabClick(
gesture: *c.GtkGestureClick,
_: c.gint,
_: c.gdouble,
_: c.gdouble,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Tab = @ptrCast(@alignCast(ud));
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
self.closeWithConfirmation();
}
}

View File

@ -32,7 +32,6 @@ renderer: renderer.Impl = .opengl,
font_backend: font.Backend = .freetype,
/// Feature flags
adwaita: bool = false,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
@ -132,12 +131,6 @@ pub fn init(b: *std.Build) !Config {
//---------------------------------------------------------------
// Feature Flags
config.adwaita = b.option(
bool,
"gtk-adwaita",
"Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true;
config.flatpak = b.option(
bool,
"flatpak",
@ -397,7 +390,6 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't
// support all types.
step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
@ -442,7 +434,6 @@ pub fn fromOptions() Config {
.version = options.app_version,
.flatpak = options.flatpak,
.adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?,

View File

@ -450,11 +450,8 @@ pub fn add(
}
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (self.config.adwaita) {
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
step.root_module.addImport("adw", gobject.module("adw1"));
}
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
step.root_module.addImport("adw", gobject.module("adw1"));
if (self.config.x11) {
step.linkSystemLibrary2("X11", dynamic_link_opts);
@ -525,7 +522,7 @@ pub fn add(
});
gtk_builder_check.root_module.addOptions("build_options", self.options);
gtk_builder_check.root_module.addImport("gtk", gobject.module("gtk4"));
if (self.config.adwaita) gtk_builder_check.root_module.addImport("adw", gobject.module("adw1"));
gtk_builder_check.root_module.addImport("adw", gobject.module("adw1"));
for (gresource.dependencies) |pathname| {
const extension = std.fs.path.extension(pathname);

View File

@ -51,19 +51,15 @@ pub fn run(alloc: Allocator) !u8 {
gtk.gtk_get_minor_version(),
gtk.gtk_get_micro_version(),
});
if (comptime build_options.adwaita) {
try stdout.print(" - libadwaita : enabled\n", .{});
try stdout.print(" build : {s}\n", .{
gtk.ADW_VERSION_S,
});
try stdout.print(" runtime : {}.{}.{}\n", .{
gtk.adw_get_major_version(),
gtk.adw_get_minor_version(),
gtk.adw_get_micro_version(),
});
} else {
try stdout.print(" - libadwaita : disabled\n", .{});
}
try stdout.print(" - libadwaita : enabled\n", .{});
try stdout.print(" build : {s}\n", .{
gtk.ADW_VERSION_S,
});
try stdout.print(" runtime : {}.{}.{}\n", .{
gtk.adw_get_major_version(),
gtk.adw_get_minor_version(),
gtk.adw_get_micro_version(),
});
if (comptime build_options.x11) {
try stdout.print(" - libX11 : enabled\n", .{});
} else {

View File

@ -49,6 +49,7 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
// one field be used for both platforms (macOS retained the ability
// to set a radius).
.{ "background-blur-radius", "background-blur" },
.{ "adw-toolbar-style", "gtk-toolbar-style" },
});
/// The font families to use.
@ -1199,7 +1200,7 @@ keybind: Keybinds = .{},
/// * `working-directory` - Set the subtitle to the working directory of the
/// surface.
///
/// This feature is only supported on GTK with Adwaita enabled.
/// This feature is only supported on GTK.
@"window-subtitle": WindowSubtitle = .false,
/// The theme to use for the windows. Valid values:
@ -1212,8 +1213,7 @@ keybind: Keybinds = .{},
/// * `light` - Use the light theme regardless of system theme.
/// * `dark` - Use the dark theme regardless of system theme.
/// * `ghostty` - Use the background and foreground colors specified in the
/// Ghostty configuration. This is only supported on Linux builds with
/// Adwaita and `gtk-adwaita` enabled.
/// Ghostty configuration. This is only supported on Linux builds.
///
/// On macOS, if `macos-titlebar-style` is "tabs", the window theme will be
/// automatically set based on the luminosity of the terminal background color.
@ -1779,9 +1779,9 @@ keybind: Keybinds = .{},
/// Control the in-app notifications that Ghostty shows.
///
/// On Linux (GTK) with Adwaita, in-app notifications show up as toasts. Toasts
/// appear overlaid on top of the terminal window. They are used to show
/// information that is not critical but may be important.
/// On Linux (GTK), in-app notifications show up as toasts. Toasts appear
/// overlaid on top of the terminal window. They are used to show information
/// that is not critical but may be important.
///
/// Possible notifications are:
///
@ -1799,7 +1799,7 @@ keybind: Keybinds = .{},
/// A value of "false" will disable all notifications. A value of "true" will
/// enable all notifications.
///
/// This configuration only applies to GTK with Adwaita enabled.
/// This configuration only applies to GTK.
@"app-notifications": AppNotifications = .{},
/// If anything other than false, fullscreen mode on macOS will not use the
@ -2129,26 +2129,20 @@ keybind: Keybinds = .{},
@"gtk-titlebar": bool = true,
/// Determines the side of the screen that the GTK tab bar will stick to.
/// Top, bottom, left, right, and hidden are supported. The default is top.
/// Top, bottom, and hidden are supported. The default is top.
///
/// If this option has value `left` or `right` when using Adwaita, it falls
/// back to `top`. `hidden`, meaning that tabs don't exist, is not supported
/// without using Adwaita, falling back to `top`.
///
/// When `hidden` is set and Adwaita is enabled, a tab button displaying the
/// number of tabs will appear in the title bar. It has the ability to open a
/// tab overview for displaying tabs. Alternatively, you can use the
/// `toggle_tab_overview` action in a keybind if your window doesn't have a
/// title bar, or you can switch tabs with keybinds.
/// When `hidden` is set, a tab button displaying the number of tabs will appear
/// in the title bar. It has the ability to open a tab overview for displaying
/// tabs. Alternatively, you can use the `toggle_tab_overview` action in a
/// keybind if your window doesn't have a title bar, or you can switch tabs
/// with keybinds.
@"gtk-tabs-location": GtkTabsLocation = .top,
/// If this is `true`, the titlebar will be hidden when the window is maximized,
/// and shown when the titlebar is unmaximized. GTK only.
@"gtk-titlebar-hide-when-maximized": bool = false,
/// Determines the appearance of the top and bottom bars when using the
/// Adwaita tab bar. This requires `gtk-adwaita` to be enabled (it is
/// by default).
/// Determines the appearance of the top and bottom bars tab bar.
///
/// Valid values are:
///
@ -2158,7 +2152,7 @@ keybind: Keybinds = .{},
/// more subtle border.
///
/// Changing this value at runtime will only affect new windows.
@"adw-toolbar-style": AdwToolbarStyle = .raised,
@"gtk-toolbar-style": GtkToolbarStyle = .raised,
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
/// are the new typical Gnome style where tabs fill their available space.
@ -2166,20 +2160,6 @@ keybind: Keybinds = .{},
/// which is the old style.
@"gtk-wide-tabs": bool = true,
/// If `true` (default), Ghostty will enable Adwaita theme support. This
/// will make `window-theme` work properly and will also allow Ghostty to
/// properly respond to system theme changes, light/dark mode changing, etc.
/// This requires a GTK4 desktop with a GTK4 theme.
///
/// If you are running GTK3 or have a GTK3 theme, you may have to set this
/// to false to get your theme picked up properly. Having this set to true
/// with GTK3 should not cause any problems, but it may not work exactly as
/// expected.
///
/// This configuration only has an effect if Ghostty was built with
/// Adwaita support.
@"gtk-adwaita": bool = true,
/// Custom CSS files to be loaded.
///
/// This configuration can be repeated multiple times to load multiple files.
@ -5758,13 +5738,11 @@ pub const GtkSingleInstance = enum {
pub const GtkTabsLocation = enum {
top,
bottom,
left,
right,
hidden,
};
/// See adw-toolbar-style
pub const AdwToolbarStyle = enum {
/// See gtk-toolbar-style
pub const GtkToolbarStyle = enum {
flat,
raised,
@"raised-border",