From 25c5ecf553a1460ad88607c838e2f282612d1b9f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 30 Jan 2025 22:59:36 -0600 Subject: [PATCH] gtk: require libadwaita This commit removes support for building without libadwaita. --- .github/workflows/test.yml | 6 +- src/apprt/gtk/App.zig | 151 ++++---------- src/apprt/gtk/Window.zig | 208 +++++++----------- src/apprt/gtk/adwaita.zig | 17 -- src/apprt/gtk/builder_check.zig | 11 +- src/apprt/gtk/c.zig | 4 +- src/apprt/gtk/headerbar.zig | 91 ++++---- src/apprt/gtk/headerbar_adw.zig | 78 ------- src/apprt/gtk/headerbar_gtk.zig | 52 ----- src/apprt/gtk/notebook.zig | 359 +++++++++++++++++++------------- src/apprt/gtk/notebook_adw.zig | 209 ------------------- src/apprt/gtk/notebook_gtk.zig | 304 --------------------------- src/build/Config.zig | 9 - src/build/SharedDeps.zig | 9 +- src/cli/version.zig | 22 +- src/config/Config.zig | 56 ++--- 16 files changed, 416 insertions(+), 1170 deletions(-) delete mode 100644 src/apprt/gtk/headerbar_adw.zig delete mode 100644 src/apprt/gtk/headerbar_gtk.zig delete mode 100644 src/apprt/gtk/notebook_adw.zig delete mode 100644 src/apprt/gtk/notebook_gtk.zig diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2082d7286..41a54fde3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 985ce92b3..227c36ec4 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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(); } }, } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3daeffe76..bbf53b351 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -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(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(self.window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT); + _ = 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) 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(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(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 ((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", diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig index 075055586..885627fa4 100644 --- a/src/apprt/gtk/adwaita.zig +++ b/src/apprt/gtk/adwaita.zig @@ -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 diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig index f042c89a9..015c6310d 100644 --- a/src/apprt/gtk/builder_check.zig +++ b/src/apprt/gtk/builder_check.zig @@ -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(); diff --git a/src/apprt/gtk/c.zig b/src/apprt/gtk/c.zig index 5bd32edfe..c42c35d46 100644 --- a/src/apprt/gtk/c.zig +++ b/src/apprt/gtk/c.zig @@ -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 diff --git a/src/apprt/gtk/headerbar.zig b/src/apprt/gtk/headerbar.zig index 0f7f15bf8..8f4c58fc4 100644 --- a/src/apprt/gtk/headerbar.zig +++ b/src/apprt/gtk/headerbar.zig @@ -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); +} diff --git a/src/apprt/gtk/headerbar_adw.zig b/src/apprt/gtk/headerbar_adw.zig deleted file mode 100644 index 1ae23e6d9..000000000 --- a/src/apprt/gtk/headerbar_adw.zig +++ /dev/null @@ -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); - } -} diff --git a/src/apprt/gtk/headerbar_gtk.zig b/src/apprt/gtk/headerbar_gtk.zig deleted file mode 100644 index 63ba8b389..000000000 --- a/src/apprt/gtk/headerbar_gtk.zig +++ /dev/null @@ -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 {} diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 548f2acaf..e411ba9ad 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -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)); +} diff --git a/src/apprt/gtk/notebook_adw.zig b/src/apprt/gtk/notebook_adw.zig deleted file mode 100644 index 790b3aa35..000000000 --- a/src/apprt/gtk/notebook_adw.zig +++ /dev/null @@ -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)); -} diff --git a/src/apprt/gtk/notebook_gtk.zig b/src/apprt/gtk/notebook_gtk.zig deleted file mode 100644 index 5f145dc84..000000000 --- a/src/apprt/gtk/notebook_gtk.zig +++ /dev/null @@ -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(>kPageAdded), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gtk_notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gtk_notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gtk_notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), 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(>kTabCloseClick), tab, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), 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(); - } -} diff --git a/src/build/Config.zig b/src/build/Config.zig index 7c8605c73..f7bf96d36 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -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)).?, diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 7d0b64c5b..a90fc330a 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -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); diff --git a/src/cli/version.zig b/src/cli/version.zig index 4a6af242c..f6d2ea9df 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -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 { diff --git a/src/config/Config.zig b/src/config/Config.zig index d191de53a..705e4f0df 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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",