mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
adw: move notebook to its own file
This commit is contained in:
@ -92,7 +92,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
||||
// Add Surface to the Tab
|
||||
c.gtk_box_append(self.box, surface.primaryWidget());
|
||||
|
||||
try window.notebook.addTab(box_widget, "Ghostty");
|
||||
try window.notebook.addTab(self, "Ghostty");
|
||||
|
||||
// const notebook: *c.GtkNotebook = window.notebook.as_notebook();
|
||||
|
||||
@ -132,12 +132,9 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
||||
|
||||
// // 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(notebook, page_idx);
|
||||
|
||||
// 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.
|
||||
surface.grabFocus();
|
||||
@ -166,15 +163,7 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
|
||||
}
|
||||
|
||||
pub fn setLabelText(self: *Tab, title: [:0]const u8) void {
|
||||
switch (self.window.notebook) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(self.box));
|
||||
c.adw_tab_page_set_title(page, title.ptr);
|
||||
},
|
||||
.gtk_notebook => {
|
||||
c.gtk_label_set_text(self.label_text, title.ptr);
|
||||
}
|
||||
}
|
||||
self.window.notebook.setTabLabel(self, title);
|
||||
}
|
||||
|
||||
/// Remove this tab from the window.
|
||||
|
@ -21,134 +21,10 @@ const Color = configpkg.Config.Color;
|
||||
const Surface = @import("Surface.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const Notebook = @import("./notebook.zig").Notebook;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const Notebook = union(enum) {
|
||||
adw_tab_view: *c.AdwTabView,
|
||||
gtk_notebook: *c.GtkNotebook,
|
||||
|
||||
pub fn create(window: *Window) @This() {
|
||||
const app = window.app;
|
||||
|
||||
// Create a notebook to hold our tabs.
|
||||
const notebook_widget = 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);
|
||||
|
||||
// If we are in fullscreen mode, new windows start fullscreen.
|
||||
if (app.config.fullscreen) c.gtk_window_fullscreen(window.window);
|
||||
|
||||
// All of our events
|
||||
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = 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);
|
||||
|
||||
return .{ .gtk_notebook = notebook };
|
||||
}
|
||||
|
||||
pub fn as_widget(self: Notebook) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)),
|
||||
.gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn as_notebook(self: Notebook) *c.GtkNotebook {
|
||||
return switch (self) {
|
||||
.adw_tab_view => @panic("adw tab view"),
|
||||
.gtk_notebook => |notebook| notebook,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn nPages(self: Notebook) c_int {
|
||||
return switch (self) {
|
||||
.adw_tab_view => |tab_view| c.adw_tab_view_get_n_pages(tab_view),
|
||||
.gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn currentPage(self: Notebook) c_int {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
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: {
|
||||
const page = c.adw_tab_view_get_selected_page(tab_view);
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
break :child child;
|
||||
},
|
||||
.gtk_notebook => |notebook| child: {
|
||||
const page = self.currentPage();
|
||||
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 addTab(self: Notebook, tab_widget: *c.GtkWidget, title: [:0]const u8) !void {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
const page = c.adw_tab_view_append(tab_view, tab_widget);
|
||||
c.adw_tab_page_set_title(page, title);
|
||||
},
|
||||
.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("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);
|
||||
|
||||
const parent_page_idx = self.nPages();
|
||||
const page_idx = c.gtk_notebook_insert_page(
|
||||
notebook,
|
||||
tab_widget,
|
||||
label_box_widget,
|
||||
parent_page_idx,
|
||||
);
|
||||
_ = page_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
app: *App,
|
||||
|
||||
/// Our window
|
||||
@ -246,17 +122,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_box_append(@ptrCast(box), warning);
|
||||
}
|
||||
|
||||
const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita";
|
||||
if (adwaita) {
|
||||
log.warn("using adwaita", .{});
|
||||
const tab_view = c.adw_tab_view_new();
|
||||
const tab_bar = c.adw_tab_bar_new();
|
||||
c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar)));
|
||||
c.adw_tab_bar_set_view(tab_bar, tab_view.?);
|
||||
self.notebook = .{ .adw_tab_view = tab_view.? };
|
||||
} else {
|
||||
self.notebook = Notebook.create(self);
|
||||
}
|
||||
self.notebook = Notebook.create(self, box);
|
||||
c.gtk_box_append(@ptrCast(box), self.notebook.as_widget());
|
||||
|
||||
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
|
||||
@ -331,34 +197,12 @@ pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
||||
/// Close the tab for the given notebook page. This will automatically
|
||||
/// handle closing the window if there are no more tabs.
|
||||
pub fn closeTab(self: *Window, tab: *Tab) void {
|
||||
const notebook: *c.GtkNotebook = self.notebook.as_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.
|
||||
c.gtk_notebook_remove_page(notebook, page_idx);
|
||||
|
||||
const remaining = c.gtk_notebook_get_n_pages(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(notebook, 0),
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// If we have remaining tabs, we need to make sure we grab focus.
|
||||
if (remaining > 0) self.focusCurrentTab();
|
||||
self.notebook.closeTab(tab);
|
||||
}
|
||||
|
||||
/// Returns true if this window has any tabs.
|
||||
pub fn hasTabs(self: *const Window) bool {
|
||||
return c.gtk_notebook_get_n_pages(self.notebook.as_notebook()) > 0;
|
||||
return self.notebook.nPages() > 0;
|
||||
}
|
||||
|
||||
/// Go to the previous tab for a surface.
|
||||
@ -368,9 +212,9 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
||||
return;
|
||||
};
|
||||
|
||||
const notebook: *c.GtkNotebook = self.notebook.as_notebook();
|
||||
const notebook: *c.GtkNotebook = self.notebook.gtk_notebook;
|
||||
const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return;
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
const page_idx = Notebook.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: {
|
||||
@ -392,9 +236,9 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void {
|
||||
return;
|
||||
};
|
||||
|
||||
const notebook: *c.GtkNotebook = self.notebook.as_notebook();
|
||||
const notebook: *c.GtkNotebook = self.notebook.gtk_notebook;
|
||||
const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return;
|
||||
const page_idx = getNotebookPageIndex(page);
|
||||
const page_idx = Notebook.getNotebookPageIndex(page);
|
||||
const max = c.gtk_notebook_get_n_pages(notebook) -| 1;
|
||||
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||
if (next_idx == page_idx) return;
|
||||
@ -405,15 +249,15 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void {
|
||||
|
||||
/// Go to the next tab for a surface.
|
||||
pub fn gotoLastTab(self: *Window) void {
|
||||
const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1;
|
||||
c.gtk_notebook_set_current_page(self.notebook, max);
|
||||
const max = self.notebook.nPages() -| 1;
|
||||
c.gtk_notebook_set_current_page(self.notebook.gtk_notebook, max);
|
||||
self.focusCurrentTab();
|
||||
}
|
||||
|
||||
/// Go to the specific tab index.
|
||||
pub fn gotoTab(self: *Window, n: usize) void {
|
||||
if (n == 0) return;
|
||||
const notebook: *c.GtkNotebook = self.notebook.as_notebook();
|
||||
const notebook: *c.GtkNotebook = self.notebook.gtk_notebook;
|
||||
const max = c.gtk_notebook_get_n_pages(notebook);
|
||||
const page_idx = std.math.cast(c_int, n - 1) orelse return;
|
||||
if (page_idx < max) {
|
||||
@ -442,13 +286,7 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
||||
}
|
||||
|
||||
/// Grabs focus on the currently selected tab.
|
||||
fn focusCurrentTab(self: *Window) void {
|
||||
// const notebook: *c.GtkNotebook = self.notebook.as_notebook();
|
||||
// const page_idx = c.gtk_notebook_get_current_page(notebook);
|
||||
// const page = c.gtk_notebook_get_nth_page(notebook, page_idx);
|
||||
// const tab: *Tab = @ptrCast(@alignCast(
|
||||
// c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
|
||||
// ));
|
||||
pub fn focusCurrentTab(self: *Window) void {
|
||||
const tab = self.notebook.currentTab() orelse return;
|
||||
const gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area));
|
||||
_ = c.gtk_widget_grab_focus(gl_area);
|
||||
@ -465,81 +303,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.?);
|
||||
|
||||
const notebook: *c.GtkNotebook = self.notebook.as_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 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.as_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.as_notebook();
|
||||
}
|
||||
|
||||
fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
_ = v;
|
||||
log.debug("refocus term request", .{});
|
||||
@ -621,19 +384,6 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
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(
|
||||
_: *c.GSimpleAction,
|
||||
_: *c.GVariant,
|
||||
@ -773,7 +523,7 @@ fn gtkActionReset(
|
||||
/// Returns the surface to use for an action.
|
||||
fn actionSurface(self: *Window) ?*CoreSurface {
|
||||
const tab = self.notebook.currentTab() orelse return null;
|
||||
// const notebook: *c.GtkNotebook = self.notebook.as_notebook();
|
||||
// const notebook: *c.GtkNotebook = self.notebook.gtk_notebook;
|
||||
// const page_idx = c.gtk_notebook_get_current_page(notebook);
|
||||
// const page = c.gtk_notebook_get_nth_page(notebook, page_idx);
|
||||
// const tab: *Tab = @ptrCast(@alignCast(
|
||||
@ -782,6 +532,6 @@ fn actionSurface(self: *Window) ?*CoreSurface {
|
||||
return &tab.focus_child.core_surface;
|
||||
}
|
||||
|
||||
fn userdataSelf(ud: *anyopaque) *Window {
|
||||
pub fn userdataSelf(ud: *anyopaque) *Window {
|
||||
return @ptrCast(@alignCast(ud));
|
||||
}
|
||||
|
330
src/apprt/gtk/notebook.zig
Normal file
330
src/apprt/gtk/notebook.zig
Normal file
@ -0,0 +1,330 @@
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const Window = @import("./Window.zig");
|
||||
const userdataSelf = Window.userdataSelf;
|
||||
const Tab = @import("./Tab.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const AdwTabView = if (build_options.libadwaita) c.AdwTabView else anyopaque;
|
||||
|
||||
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;
|
||||
|
||||
const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita";
|
||||
|
||||
if (adwaita) {
|
||||
log.warn("using adwaita", .{});
|
||||
const tab_view = c.adw_tab_view_new();
|
||||
const tab_bar = c.adw_tab_bar_new();
|
||||
c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar)));
|
||||
c.adw_tab_bar_set_view(tab_bar, 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, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
return .{ .adw_tab_view = tab_view.? };
|
||||
}
|
||||
|
||||
// Create a notebook to hold our tabs.
|
||||
const notebook_widget = 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);
|
||||
|
||||
// If we are in fullscreen mode, new windows start fullscreen.
|
||||
if (app.config.fullscreen) c.gtk_window_fullscreen(window.window);
|
||||
|
||||
// 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);
|
||||
|
||||
return .{ .gtk_notebook = notebook };
|
||||
}
|
||||
|
||||
pub fn as_widget(self: Notebook) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)),
|
||||
.gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn nPages(self: Notebook) c_int {
|
||||
return switch (self) {
|
||||
.adw_tab_view => |tab_view| if (build_options.libadwaita) c.adw_tab_view_get_n_pages(tab_view) else unreachable,
|
||||
.gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn currentPage(self: Notebook) c_int {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (!build_options.libadwaita) 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 (!build_options.libadwaita) 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;
|
||||
log.info("currentPage_page_idx = {}", .{page});
|
||||
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 setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (!build_options.libadwaita) 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 (!build_options.libadwaita) 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("Ghostty");
|
||||
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
|
||||
c.gtk_box_append(label_box, label_text_widget);
|
||||
tab.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);
|
||||
|
||||
const parent_page_idx = self.nPages();
|
||||
const page_idx = c.gtk_notebook_insert_page(
|
||||
notebook,
|
||||
box_widget,
|
||||
label_box_widget,
|
||||
parent_page_idx,
|
||||
);
|
||||
|
||||
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 {
|
||||
switch (self) {
|
||||
.adw_tab_view => |tab_view| {
|
||||
if (!build_options.libadwaita) 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)
|
||||
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);
|
||||
|
||||
log.info("page_idx = {}", .{page_idx});
|
||||
|
||||
// Remove the page. This will destroy the GTK widgets in the page which
|
||||
// will trigger Tab cleanup.
|
||||
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) tab.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 = userdataSelf(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 = userdataSelf(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 = 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 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.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(self.window, label_text);
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *AdwTabView,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) ?*AdwTabView {
|
||||
const currentWindow = userdataSelf(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 = userdataSelf(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);
|
||||
}
|
Reference in New Issue
Block a user