mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #2051 from Pangoraw/adw_tab_view
gtk: use Adwaita TabView when possible
This commit is contained in:
@ -21,8 +21,7 @@ const Config = configpkg.Config;
|
|||||||
const CoreApp = @import("../../App.zig");
|
const CoreApp = @import("../../App.zig");
|
||||||
const CoreSurface = @import("../../Surface.zig");
|
const CoreSurface = @import("../../Surface.zig");
|
||||||
|
|
||||||
const build_options = @import("build_options");
|
const adwaita = @import("adwaita.zig");
|
||||||
|
|
||||||
const cgroup = @import("cgroup.zig");
|
const cgroup = @import("cgroup.zig");
|
||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
@ -108,6 +107,18 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're using libadwaita, log the version
|
||||||
|
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and
|
||||||
|
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
|
// The "none" cursor is used for hiding the cursor
|
||||||
const cursor_none = c.gdk_cursor_new_from_name("none", null);
|
const cursor_none = c.gdk_cursor_new_from_name("none", null);
|
||||||
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
|
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
|
||||||
@ -143,8 +154,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
|
|
||||||
// Create our GTK Application which encapsulates our process.
|
// Create our GTK Application which encapsulates our process.
|
||||||
const app: *c.GtkApplication = app: {
|
const app: *c.GtkApplication = app: {
|
||||||
const adwaita = build_options.libadwaita and config.@"gtk-adwaita";
|
|
||||||
|
|
||||||
log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
|
log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
|
||||||
app_id,
|
app_id,
|
||||||
single_instance,
|
single_instance,
|
||||||
@ -152,10 +161,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// If not libadwaita, create a standard GTK application.
|
// If not libadwaita, create a standard GTK application.
|
||||||
if (!adwaita) break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
|
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and
|
||||||
app_id.ptr,
|
!adwaita.enabled(&config))
|
||||||
app_flags,
|
{
|
||||||
))) orelse return error.GtkInitFailed;
|
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
|
// Use libadwaita if requested. Using an AdwApplication lets us use
|
||||||
// Adwaita widgets and access things such as the color scheme.
|
// Adwaita widgets and access things such as the color scheme.
|
||||||
|
@ -894,7 +894,7 @@ fn updateTitleLabels(self: *Surface) void {
|
|||||||
|
|
||||||
// If we have a tab and are the focused child, then we have to update the tab
|
// If we have a tab and are the focused child, then we have to update the tab
|
||||||
if (self.container.tab()) |tab| {
|
if (self.container.tab()) |tab| {
|
||||||
if (tab.focus_child == self) c.gtk_label_set_text(tab.label_text, title.ptr);
|
if (tab.focus_child == self) tab.setLabelText(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a window and are focused, then we have to update the window title.
|
// If we have a window and are focused, then we have to update the window title.
|
||||||
|
@ -55,38 +55,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
|||||||
.focus_child = undefined,
|
.focus_child = undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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("Ghostty");
|
|
||||||
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
|
|
||||||
c.gtk_box_append(label_box, label_text_widget);
|
|
||||||
self.label_text = label_text;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Wide style GTK tabs
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Box in which we'll later keep either Surface or Split.
|
// Create a Box in which we'll later keep either Surface or Split.
|
||||||
// Using a box makes it easier to maintain the tab contents because
|
// Using a box makes it easier to maintain the tab contents because
|
||||||
// we never need to change the root widget of the notebook page (tab).
|
// we never need to change the root widget of the notebook page (tab).
|
||||||
@ -106,47 +74,12 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
|||||||
// Add Surface to the Tab
|
// Add Surface to the Tab
|
||||||
c.gtk_box_append(self.box, surface.primaryWidget());
|
c.gtk_box_append(self.box, surface.primaryWidget());
|
||||||
|
|
||||||
// Add the notebook page (create tab).
|
|
||||||
const parent_page_idx = switch (window.app.config.@"window-new-tab-position") {
|
|
||||||
.current => c.gtk_notebook_get_current_page(window.notebook) + 1,
|
|
||||||
.end => c.gtk_notebook_get_n_pages(window.notebook),
|
|
||||||
};
|
|
||||||
|
|
||||||
const page_idx = c.gtk_notebook_insert_page(
|
|
||||||
window.notebook,
|
|
||||||
box_widget,
|
|
||||||
label_box_widget,
|
|
||||||
parent_page_idx,
|
|
||||||
);
|
|
||||||
if (page_idx < 0) {
|
|
||||||
log.warn("failed to add page to notebook", .{});
|
|
||||||
return error.GtkAppendPageFailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab settings
|
|
||||||
c.gtk_notebook_set_tab_reorderable(window.notebook, box_widget, 1);
|
|
||||||
c.gtk_notebook_set_tab_detachable(window.notebook, box_widget, 1);
|
|
||||||
|
|
||||||
// If we have multiple tabs, show the tab bar.
|
|
||||||
if (c.gtk_notebook_get_n_pages(window.notebook) > 1) {
|
|
||||||
c.gtk_notebook_set_show_tabs(window.notebook, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the userdata of the box to point to this tab.
|
// Set the userdata of the box to point to this tab.
|
||||||
c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self);
|
c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self);
|
||||||
|
try window.notebook.addTab(self, "Ghostty");
|
||||||
// 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));
|
|
||||||
|
|
||||||
// Attach all events
|
// Attach all events
|
||||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
_ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
|
|
||||||
// Switch to the new tab
|
|
||||||
c.gtk_notebook_set_current_page(window.notebook, page_idx);
|
|
||||||
|
|
||||||
// We need to grab focus after Surface and Tab is added to the window. When
|
// We need to grab focus after Surface and Tab is added to the window. When
|
||||||
// creating a Tab we want to always focus on the widget.
|
// creating a Tab we want to always focus on the widget.
|
||||||
@ -175,12 +108,16 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
|
|||||||
self.elem = elem;
|
self.elem = elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setLabelText(self: *Tab, title: [:0]const u8) void {
|
||||||
|
self.window.notebook.setTabLabel(self, title);
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove this tab from the window.
|
/// Remove this tab from the window.
|
||||||
pub fn remove(self: *Tab) void {
|
pub fn remove(self: *Tab) void {
|
||||||
self.window.closeTab(self);
|
self.window.closeTab(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||||
const window = tab.window;
|
const window = tab.window;
|
||||||
window.closeTab(tab);
|
window.closeTab(tab);
|
||||||
@ -195,7 +132,7 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|||||||
tab.destroy(tab.window.app.core_app.alloc);
|
tab.destroy(tab.window.app.core_app.alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkTabClick(
|
pub fn gtkTabClick(
|
||||||
gesture: *c.GtkGestureClick,
|
gesture: *c.GtkGestureClick,
|
||||||
_: c.gint,
|
_: c.gint,
|
||||||
_: c.gdouble,
|
_: c.gdouble,
|
||||||
|
@ -20,6 +20,8 @@ const Color = configpkg.Config.Color;
|
|||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
const Tab = @import("Tab.zig");
|
const Tab = @import("Tab.zig");
|
||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
|
const adwaita = @import("adwaita.zig");
|
||||||
|
const Notebook = @import("./notebook.zig").Notebook;
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
@ -29,7 +31,8 @@ app: *App,
|
|||||||
window: *c.GtkWindow,
|
window: *c.GtkWindow,
|
||||||
|
|
||||||
/// The notebook (tab grouping) for this window.
|
/// The notebook (tab grouping) for this window.
|
||||||
notebook: *c.GtkNotebook,
|
/// can be either c.GtkNotebook or c.AdwTabView.
|
||||||
|
notebook: Notebook,
|
||||||
|
|
||||||
context_menu: *c.GtkWidget,
|
context_menu: *c.GtkWidget,
|
||||||
|
|
||||||
@ -57,9 +60,18 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create the window
|
// Create the window
|
||||||
const window = c.gtk_application_window_new(app.app);
|
const adw_window =
|
||||||
|
(comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||||
|
adwaita.enabled(&app.config) and
|
||||||
|
app.config.@"gtk-titlebar" and
|
||||||
|
adwaita.versionAtLeast(1, 4, 0);
|
||||||
|
const window: *c.GtkWidget = if (adw_window)
|
||||||
|
c.adw_application_window_new(app.app)
|
||||||
|
else
|
||||||
|
c.gtk_application_window_new(app.app);
|
||||||
|
|
||||||
const gtk_window: *c.GtkWindow = @ptrCast(window);
|
const gtk_window: *c.GtkWindow = @ptrCast(window);
|
||||||
errdefer c.gtk_window_destroy(gtk_window);
|
errdefer if (adw_window) c.adw_application_window_destroy(window) else c.gtk_application_window_destroy(gtk_window);
|
||||||
self.window = gtk_window;
|
self.window = gtk_window;
|
||||||
c.gtk_window_set_title(gtk_window, "Ghostty");
|
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||||
@ -75,6 +87,8 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity");
|
c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var header: ?*c.GtkHeaderBar = null;
|
||||||
|
|
||||||
// Internally, GTK ensures that only one instance of this provider exists in the provider list
|
// Internally, GTK ensures that only one instance of this provider exists in the provider list
|
||||||
// for the display.
|
// for the display.
|
||||||
const display = c.gdk_display_get_default();
|
const display = c.gdk_display_get_default();
|
||||||
@ -85,8 +99,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
// are decorated or not because we can have a keybind to toggle the
|
// are decorated or not because we can have a keybind to toggle the
|
||||||
// decorations.
|
// decorations.
|
||||||
if (app.config.@"gtk-titlebar") {
|
if (app.config.@"gtk-titlebar") {
|
||||||
const header = c.gtk_header_bar_new();
|
header = @ptrCast(c.gtk_header_bar_new());
|
||||||
c.gtk_window_set_titlebar(gtk_window, header);
|
|
||||||
{
|
{
|
||||||
const btn = c.gtk_menu_button_new();
|
const btn = c.gtk_menu_button_new();
|
||||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||||
@ -107,41 +120,29 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.gtk_window_set_decorated(gtk_window, 0);
|
c.gtk_window_set_decorated(gtk_window, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a notebook to hold our tabs.
|
|
||||||
const notebook_widget = c.gtk_notebook_new();
|
|
||||||
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
|
|
||||||
self.notebook = notebook;
|
|
||||||
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
|
|
||||||
.top => 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(notebook, notebook_tab_pos);
|
|
||||||
c.gtk_notebook_set_scrollable(notebook, 1);
|
|
||||||
c.gtk_notebook_set_show_tabs(notebook, 0);
|
|
||||||
c.gtk_notebook_set_show_border(notebook, 0);
|
|
||||||
|
|
||||||
// This enables all Ghostty terminal tabs to be exchanged across windows.
|
|
||||||
c.gtk_notebook_set_group_name(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);
|
|
||||||
|
|
||||||
// Create our box which will hold our widgets.
|
// Create our box which will hold our widgets.
|
||||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
|
||||||
// In debug we show a warning. This is a really common issue where
|
// In debug we show a warning and apply the 'devel' class to the window.
|
||||||
// people build from source in debug and performance is really bad.
|
// This is a really common issue where people build from source in debug and performance is really bad.
|
||||||
if (comptime std.debug.runtime_safety) {
|
if (comptime std.debug.runtime_safety) {
|
||||||
const warning = c.gtk_label_new("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
|
const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded.";
|
||||||
c.gtk_widget_set_margin_top(warning, 10);
|
if ((comptime adwaita.versionAtLeast(1, 3, 0)) and
|
||||||
c.gtk_widget_set_margin_bottom(warning, 10);
|
adwaita.enabled(&app.config) and
|
||||||
c.gtk_box_append(@ptrCast(box), warning);
|
adwaita.versionAtLeast(1, 3, 0))
|
||||||
|
{
|
||||||
|
const banner = c.adw_banner_new(warning_text);
|
||||||
|
c.gtk_box_append(@ptrCast(box), @ptrCast(banner));
|
||||||
|
} else {
|
||||||
|
const warning = c.gtk_label_new(warning_text);
|
||||||
|
c.gtk_widget_set_margin_top(warning, 10);
|
||||||
|
c.gtk_widget_set_margin_bottom(warning, 10);
|
||||||
|
c.gtk_box_append(@ptrCast(box), warning);
|
||||||
|
}
|
||||||
|
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel");
|
||||||
}
|
}
|
||||||
c.gtk_box_append(@ptrCast(box), notebook_widget);
|
|
||||||
|
self.notebook = Notebook.create(self, box);
|
||||||
|
|
||||||
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
|
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
|
||||||
c.gtk_widget_set_parent(self.context_menu, window);
|
c.gtk_widget_set_parent(self.context_menu, window);
|
||||||
@ -155,16 +156,39 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
|
_ = 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, "close-request", c.G_CALLBACK(>kCloseRequest), 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(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
_ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
_ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
|
|
||||||
// Our actions for the menu
|
// Our actions for the menu
|
||||||
initActions(self);
|
initActions(self);
|
||||||
|
|
||||||
// The box is our main child
|
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||||
c.gtk_window_set_child(gtk_window, box);
|
adwaita.enabled(&app.config) and
|
||||||
|
adwaita.versionAtLeast(1, 4, 0) and
|
||||||
|
app.config.@"gtk-titlebar" and
|
||||||
|
header != null)
|
||||||
|
{
|
||||||
|
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
|
||||||
|
|
||||||
|
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(header.?));
|
||||||
|
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
|
||||||
|
const tab_bar = c.adw_tab_bar_new();
|
||||||
|
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_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 is not supported in libadwaita.
|
||||||
|
.top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
|
||||||
|
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget),
|
||||||
|
}
|
||||||
|
c.adw_toolbar_view_set_content(toolbar_view, box);
|
||||||
|
|
||||||
|
c.adw_application_window_set_content(@ptrCast(gtk_window), @ptrCast(@alignCast(toolbar_view)));
|
||||||
|
} else {
|
||||||
|
// The box is our main child
|
||||||
|
c.gtk_window_set_child(gtk_window, box);
|
||||||
|
if (header) |h| c.gtk_window_set_titlebar(gtk_window, @ptrCast(@alignCast(h)));
|
||||||
|
}
|
||||||
|
|
||||||
// Show the window
|
// Show the window
|
||||||
c.gtk_widget_show(window);
|
c.gtk_widget_show(window);
|
||||||
@ -219,33 +243,12 @@ pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
|||||||
/// Close the tab for the given notebook page. This will automatically
|
/// Close the tab for the given notebook page. This will automatically
|
||||||
/// handle closing the window if there are no more tabs.
|
/// handle closing the window if there are no more tabs.
|
||||||
pub fn closeTab(self: *Window, tab: *Tab) void {
|
pub fn closeTab(self: *Window, tab: *Tab) void {
|
||||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
self.notebook.closeTab(tab);
|
||||||
|
|
||||||
// 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.
|
|
||||||
c.gtk_notebook_remove_page(self.notebook, page_idx);
|
|
||||||
|
|
||||||
const remaining = c.gtk_notebook_get_n_pages(self.notebook);
|
|
||||||
switch (remaining) {
|
|
||||||
// If we have no more tabs we close the window
|
|
||||||
0 => c.gtk_window_destroy(self.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.focusCurrentTab();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this window has any tabs.
|
/// Returns true if this window has any tabs.
|
||||||
pub fn hasTabs(self: *const Window) bool {
|
pub fn hasTabs(self: *const Window) bool {
|
||||||
return c.gtk_notebook_get_n_pages(self.notebook) > 1;
|
return self.notebook.nPages() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go to the previous tab for a surface.
|
/// Go to the previous tab for a surface.
|
||||||
@ -254,20 +257,7 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
|||||||
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
self.notebook.gotoPreviousTab(tab);
|
||||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
|
||||||
const page_idx = getNotebookPageIndex(page);
|
|
||||||
|
|
||||||
// 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 = c.gtk_notebook_get_n_pages(self.notebook);
|
|
||||||
break :next_idx max -| 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Do nothing if we have one tab
|
|
||||||
if (next_idx == page_idx) return;
|
|
||||||
|
|
||||||
c.gtk_notebook_set_current_page(self.notebook, next_idx);
|
|
||||||
self.focusCurrentTab();
|
self.focusCurrentTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,31 +267,23 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void {
|
|||||||
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
self.notebook.gotoNextTab(tab);
|
||||||
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
|
|
||||||
const page_idx = getNotebookPageIndex(page);
|
|
||||||
const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1;
|
|
||||||
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
|
||||||
if (next_idx == page_idx) return;
|
|
||||||
|
|
||||||
c.gtk_notebook_set_current_page(self.notebook, next_idx);
|
|
||||||
self.focusCurrentTab();
|
self.focusCurrentTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go to the next tab for a surface.
|
/// Go to the next tab for a surface.
|
||||||
pub fn gotoLastTab(self: *Window) void {
|
pub fn gotoLastTab(self: *Window) void {
|
||||||
const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1;
|
const max = self.notebook.nPages() -| 1;
|
||||||
c.gtk_notebook_set_current_page(self.notebook, max);
|
self.gotoTab(@intCast(max));
|
||||||
self.focusCurrentTab();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go to the specific tab index.
|
/// Go to the specific tab index.
|
||||||
pub fn gotoTab(self: *Window, n: usize) void {
|
pub fn gotoTab(self: *Window, n: usize) void {
|
||||||
if (n == 0) return;
|
if (n == 0) return;
|
||||||
const max = c.gtk_notebook_get_n_pages(self.notebook);
|
const max = self.notebook.nPages();
|
||||||
const page_idx = std.math.cast(c_int, n - 1) orelse return;
|
const page_idx = std.math.cast(c_int, n - 1) orelse return;
|
||||||
if (page_idx < max) {
|
if (page_idx < max) {
|
||||||
c.gtk_notebook_set_current_page(self.notebook, page_idx);
|
self.notebook.gotoNthTab(page_idx);
|
||||||
self.focusCurrentTab();
|
self.focusCurrentTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -326,12 +308,8 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Grabs focus on the currently selected tab.
|
/// Grabs focus on the currently selected tab.
|
||||||
fn focusCurrentTab(self: *Window) void {
|
pub fn focusCurrentTab(self: *Window) void {
|
||||||
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
|
const tab = self.notebook.currentTab() orelse return;
|
||||||
const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
|
|
||||||
const tab: *Tab = @ptrCast(@alignCast(
|
|
||||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
|
|
||||||
));
|
|
||||||
const gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area));
|
const gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area));
|
||||||
_ = c.gtk_widget_grab_focus(gl_area);
|
_ = c.gtk_widget_grab_focus(gl_area);
|
||||||
}
|
}
|
||||||
@ -347,79 +325,6 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkPageAdded(
|
|
||||||
notebook: *c.GtkNotebook,
|
|
||||||
_: *c.GtkWidget,
|
|
||||||
page_idx: c.guint,
|
|
||||||
ud: ?*anyopaque,
|
|
||||||
) callconv(.C) void {
|
|
||||||
const self = userdataSelf(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 {
|
|
||||||
const self = userdataSelf(ud.?);
|
|
||||||
|
|
||||||
// Hide the tab bar if we only have one tab after removal
|
|
||||||
const remaining = c.gtk_notebook_get_n_pages(self.notebook);
|
|
||||||
if (remaining == 1) {
|
|
||||||
c.gtk_notebook_set_show_tabs(self.notebook, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
|
|
||||||
const self = userdataSelf(ud.?);
|
|
||||||
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);
|
|
||||||
c.gtk_window_set_title(self.window, 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 = userdataSelf(ud.?);
|
|
||||||
const alloc = currentWindow.app.core_app.alloc;
|
|
||||||
const app = currentWindow.app;
|
|
||||||
|
|
||||||
// Create a new window
|
|
||||||
const window = Window.create(alloc, app) catch |err| {
|
|
||||||
log.warn("error creating new window error={}", .{err});
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// And add it to the new window.
|
|
||||||
tab.window = window;
|
|
||||||
|
|
||||||
return window.notebook;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||||
_ = v;
|
_ = v;
|
||||||
log.debug("refocus term request", .{});
|
log.debug("refocus term request", .{});
|
||||||
@ -501,19 +406,6 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|||||||
alloc.destroy(self);
|
alloc.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 gtkActionAbout(
|
fn gtkActionAbout(
|
||||||
_: *c.GSimpleAction,
|
_: *c.GSimpleAction,
|
||||||
_: *c.GVariant,
|
_: *c.GVariant,
|
||||||
@ -652,11 +544,7 @@ fn gtkActionReset(
|
|||||||
|
|
||||||
/// Returns the surface to use for an action.
|
/// Returns the surface to use for an action.
|
||||||
fn actionSurface(self: *Window) ?*CoreSurface {
|
fn actionSurface(self: *Window) ?*CoreSurface {
|
||||||
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
|
const tab = self.notebook.currentTab() orelse return null;
|
||||||
const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
|
|
||||||
const tab: *Tab = @ptrCast(@alignCast(
|
|
||||||
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
|
|
||||||
));
|
|
||||||
return &tab.focus_child.core_surface;
|
return &tab.focus_child.core_surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
55
src/apprt/gtk/adwaita.zig
Normal file
55
src/apprt/gtk/adwaita.zig
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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.
|
||||||
|
pub fn enabled(config: *const Config) bool {
|
||||||
|
return build_options.libadwaita 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
|
||||||
|
/// not build with libadwaita.
|
||||||
|
///
|
||||||
|
/// This can be run in both a comptime and runtime context. If it
|
||||||
|
/// is run in a comptime context, it will only check the version
|
||||||
|
/// in the headers. If it is run in a runtime context, it will
|
||||||
|
/// check the actual version of the library we are linked against.
|
||||||
|
/// So generally you probably want to do both checks!
|
||||||
|
pub fn versionAtLeast(
|
||||||
|
comptime major: u16,
|
||||||
|
comptime minor: u16,
|
||||||
|
comptime micro: u16,
|
||||||
|
) bool {
|
||||||
|
if (comptime !build_options.libadwaita) 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
|
||||||
|
// very slightly faster.
|
||||||
|
if (comptime c.ADW_MAJOR_VERSION < major or
|
||||||
|
c.ADW_MINOR_VERSION < minor or
|
||||||
|
c.ADW_MICRO_VERSION < micro) return false;
|
||||||
|
|
||||||
|
// If we're in comptime then we can't check the runtime version.
|
||||||
|
if (@inComptime()) return true;
|
||||||
|
|
||||||
|
// We use the functions instead of the constants such as
|
||||||
|
// c.ADW_MINOR_VERSION because the function gets the actual
|
||||||
|
// runtime version.
|
||||||
|
if (c.adw_get_major_version() >= major) {
|
||||||
|
if (c.adw_get_major_version() > major) return true;
|
||||||
|
if (c.adw_get_minor_version() >= minor) {
|
||||||
|
if (c.adw_get_minor_version() > minor) return true;
|
||||||
|
return c.adw_get_micro_version() >= micro;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
442
src/apprt/gtk/notebook.zig
Normal file
442
src/apprt/gtk/notebook.zig
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
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 adwaita = @import("adwaita.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
|
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
|
||||||
|
|
||||||
|
/// 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_tab_view: *AdwTabView,
|
||||||
|
gtk_notebook: *c.GtkNotebook,
|
||||||
|
|
||||||
|
pub fn create(window: *Window, box: *c.GtkWidget) @This() {
|
||||||
|
const app = window.app;
|
||||||
|
if (adwaita.enabled(&app.config)) return initAdw(window, box);
|
||||||
|
return initGtk(window, box);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initGtk(window: *Window, box: *c.GtkWidget) Notebook {
|
||||||
|
const app = window.app;
|
||||||
|
|
||||||
|
// Create a notebook to hold our tabs.
|
||||||
|
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
|
||||||
|
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
|
||||||
|
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
|
||||||
|
.top => 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(notebook, notebook_tab_pos);
|
||||||
|
c.gtk_notebook_set_scrollable(notebook, 1);
|
||||||
|
c.gtk_notebook_set_show_tabs(notebook, 0);
|
||||||
|
c.gtk_notebook_set_show_border(notebook, 0);
|
||||||
|
|
||||||
|
// This enables all Ghostty terminal tabs to be exchanged across windows.
|
||||||
|
c.gtk_notebook_set_group_name(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);
|
||||||
|
|
||||||
|
// All of our events
|
||||||
|
_ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
|
c.gtk_box_append(@ptrCast(box), notebook_widget);
|
||||||
|
return .{ .gtk_notebook = notebook };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initAdw(window: *Window, box: *c.GtkWidget) Notebook {
|
||||||
|
const app = window.app;
|
||||||
|
assert(adwaita.enabled(&app.config));
|
||||||
|
|
||||||
|
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
||||||
|
c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_view)));
|
||||||
|
if ((comptime !adwaita.versionAtLeast(1, 4, 0)) or
|
||||||
|
!adwaita.versionAtLeast(1, 4, 0) or
|
||||||
|
!app.config.@"gtk-titlebar")
|
||||||
|
{
|
||||||
|
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
|
||||||
|
switch (app.config.@"gtk-tabs-location") {
|
||||||
|
// left and right is not supported in libadwaita.
|
||||||
|
.top,
|
||||||
|
.left,
|
||||||
|
.right,
|
||||||
|
=> c.gtk_box_prepend(@ptrCast(box), @ptrCast(@alignCast(tab_bar))),
|
||||||
|
|
||||||
|
.bottom => c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))),
|
||||||
|
}
|
||||||
|
c.adw_tab_bar_set_view(tab_bar, tab_view);
|
||||||
|
|
||||||
|
if (!app.config.@"gtk-wide-tabs") {
|
||||||
|
c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = 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, "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);
|
||||||
|
|
||||||
|
return .{ .adw_tab_view = tab_view };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nPages(self: Notebook) c_int {
|
||||||
|
return switch (self) {
|
||||||
|
.gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook),
|
||||||
|
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0))
|
||||||
|
c.adw_tab_view_get_n_pages(tab_view)
|
||||||
|
else
|
||||||
|
unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn currentPage(self: Notebook) c_int {
|
||||||
|
switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
const page = c.adw_tab_view_get_selected_page(tab_view);
|
||||||
|
return c.adw_tab_view_get_page_position(tab_view, page);
|
||||||
|
},
|
||||||
|
|
||||||
|
.gtk_notebook => |notebook| return c.gtk_notebook_get_current_page(notebook),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn currentTab(self: Notebook) ?*Tab {
|
||||||
|
const child = switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| child: {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null;
|
||||||
|
const child = c.adw_tab_page_get_child(page);
|
||||||
|
break :child child;
|
||||||
|
},
|
||||||
|
|
||||||
|
.gtk_notebook => |notebook| child: {
|
||||||
|
const page = self.currentPage();
|
||||||
|
if (page == -1) return null;
|
||||||
|
break :child c.gtk_notebook_get_nth_page(notebook, 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) void {
|
||||||
|
switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
const page_to_select = c.adw_tab_view_get_nth_page(tab_view, position);
|
||||||
|
c.adw_tab_view_set_selected_page(tab_view, page_to_select);
|
||||||
|
},
|
||||||
|
.gtk_notebook => |notebook| c.gtk_notebook_set_current_page(notebook, position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTabPosition(self: Notebook, tab: *Tab) ?c_int {
|
||||||
|
return switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| page_idx: {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return null;
|
||||||
|
break :page_idx c.adw_tab_view_get_page_position(tab_view, page);
|
||||||
|
},
|
||||||
|
.gtk_notebook => |notebook| page_idx: {
|
||||||
|
const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return null;
|
||||||
|
break :page_idx getNotebookPageIndex(page);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gotoPreviousTab(self: Notebook, tab: *Tab) void {
|
||||||
|
const page_idx = self.getTabPosition(tab) orelse return;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
self.gotoNthTab(next_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gotoNextTab(self: Notebook, tab: *Tab) void {
|
||||||
|
const page_idx = self.getTabPosition(tab) orelse return;
|
||||||
|
|
||||||
|
const max = self.nPages() -| 1;
|
||||||
|
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||||
|
if (next_idx == page_idx) return;
|
||||||
|
|
||||||
|
self.gotoNthTab(next_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||||
|
switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
|
||||||
|
c.adw_tab_page_set_title(page, title.ptr);
|
||||||
|
},
|
||||||
|
.gtk_notebook => c.gtk_label_set_text(tab.label_text, title.ptr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !void {
|
||||||
|
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
|
||||||
|
switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
|
||||||
|
const page = c.adw_tab_view_append(tab_view, box_widget);
|
||||||
|
c.adw_tab_page_set_title(page, title.ptr);
|
||||||
|
|
||||||
|
// Switch to the new tab
|
||||||
|
c.adw_tab_view_set_selected_page(tab_view, page);
|
||||||
|
},
|
||||||
|
.gtk_notebook => |notebook| {
|
||||||
|
// 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 parent_page_idx = self.nPages();
|
||||||
|
const page_idx = c.gtk_notebook_insert_page(
|
||||||
|
notebook,
|
||||||
|
box_widget,
|
||||||
|
label_box_widget,
|
||||||
|
parent_page_idx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
|
if (self.nPages() > 1) {
|
||||||
|
c.gtk_notebook_set_show_tabs(notebook, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the new tab
|
||||||
|
c.gtk_notebook_set_current_page(notebook, page_idx);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn closeTab(self: Notebook, tab: *Tab) void {
|
||||||
|
const window = tab.window;
|
||||||
|
switch (self) {
|
||||||
|
.adw_tab_view => |tab_view| {
|
||||||
|
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||||
|
|
||||||
|
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return;
|
||||||
|
c.adw_tab_view_close_page(tab_view, page);
|
||||||
|
|
||||||
|
// If we have no more tabs we close the window
|
||||||
|
if (self.nPages() == 0) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.gtk_window_destroy(tab.window.window);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.gtk_notebook => |notebook| {
|
||||||
|
const page = c.gtk_notebook_get_page(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(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(notebook, 0),
|
||||||
|
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have remaining tabs, we need to make sure we grab focus.
|
||||||
|
if (remaining > 0) window.focusCurrentTab();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 gtkPageRemoved(
|
||||||
|
_: *c.GtkNotebook,
|
||||||
|
_: *c.GtkWidget,
|
||||||
|
_: c.guint,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *Window = @ptrCast(@alignCast(ud.?));
|
||||||
|
|
||||||
|
const notebook: *c.GtkNotebook = self.notebook.gtk_notebook;
|
||||||
|
|
||||||
|
// Hide the tab bar if we only have one tab after removal
|
||||||
|
const remaining = c.gtk_notebook_get_n_pages(notebook);
|
||||||
|
if (remaining == 1) {
|
||||||
|
c.gtk_notebook_set_show_tabs(notebook, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwPageAttached(tab_view: *AdwTabView, page: *c.AdwTabPage, position: c_int, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
_ = position;
|
||||||
|
_ = tab_view;
|
||||||
|
const self: *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 = self;
|
||||||
|
|
||||||
|
self.focusCurrentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
c.gtk_window_set_title(window.window, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||||
|
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(window.notebook.gtk_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);
|
||||||
|
c.gtk_window_set_title(window.window, label_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 window = createWindow(currentWindow) catch |err| {
|
||||||
|
log.warn("error creating new window error={}", .{err});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// And add it to the new window.
|
||||||
|
tab.window = window;
|
||||||
|
|
||||||
|
return window.notebook.gtk_notebook;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createWindow(currentWindow: *Window) !*Window {
|
||||||
|
const alloc = currentWindow.app.core_app.alloc;
|
||||||
|
const app = currentWindow.app;
|
||||||
|
|
||||||
|
// Create a new window
|
||||||
|
return Window.create(alloc, app);
|
||||||
|
}
|
@ -1421,6 +1421,9 @@ keybind: Keybinds = .{},
|
|||||||
|
|
||||||
/// Determines the side of the screen that the GTK tab bar will stick to.
|
/// Determines the side of the screen that the GTK tab bar will stick to.
|
||||||
/// Top, bottom, left, and right are supported. The default is top.
|
/// Top, bottom, left, and right are supported. The default is top.
|
||||||
|
///
|
||||||
|
/// If this option has value `left` or `right` when using `libadwaita`, it falls
|
||||||
|
/// back to `top`.
|
||||||
@"gtk-tabs-location": GtkTabsLocation = .top,
|
@"gtk-tabs-location": GtkTabsLocation = .top,
|
||||||
|
|
||||||
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
|
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
|
||||||
|
Reference in New Issue
Block a user