mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge 9350389ecb96be4809bed57507e96c312a7a29b2 into 3404816875d956c43376bd087a0db69cb6f3db4f
This commit is contained in:
@ -544,6 +544,7 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
|
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
|
||||||
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
|
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
|
||||||
GHOSTTY_ACTION_MOVE_TAB,
|
GHOSTTY_ACTION_MOVE_TAB,
|
||||||
|
GHOSTTY_ACTION_MOVE_TAB_TO_NEW_WINDOW,
|
||||||
GHOSTTY_ACTION_GOTO_TAB,
|
GHOSTTY_ACTION_GOTO_TAB,
|
||||||
GHOSTTY_ACTION_GOTO_SPLIT,
|
GHOSTTY_ACTION_GOTO_SPLIT,
|
||||||
GHOSTTY_ACTION_RESIZE_SPLIT,
|
GHOSTTY_ACTION_RESIZE_SPLIT,
|
||||||
|
@ -524,6 +524,8 @@ extension Ghostty {
|
|||||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||||
keySequence(app, target: target, v: action.action.key_sequence)
|
keySequence(app, target: target, v: action.action.key_sequence)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_MOVE_TAB_TO_NEW_WINDOW:
|
||||||
|
fallthrough
|
||||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||||
fallthrough
|
fallthrough
|
||||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||||
|
@ -3948,6 +3948,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
.{ .amount = position },
|
.{ .amount = position },
|
||||||
),
|
),
|
||||||
|
|
||||||
|
.move_tab_to_new_window => try self.rt_app.performAction(
|
||||||
|
.{ .surface = self },
|
||||||
|
.move_tab_to_new_window,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
|
||||||
.new_split => |direction| try self.rt_app.performAction(
|
.new_split => |direction| try self.rt_app.performAction(
|
||||||
.{ .surface = self },
|
.{ .surface = self },
|
||||||
.new_split,
|
.new_split,
|
||||||
|
@ -107,6 +107,9 @@ pub const Action = union(Key) {
|
|||||||
/// cyclically within the tab range.
|
/// cyclically within the tab range.
|
||||||
move_tab: MoveTab,
|
move_tab: MoveTab,
|
||||||
|
|
||||||
|
/// Moves the tab that contains the target surface to a new window.
|
||||||
|
move_tab_to_new_window,
|
||||||
|
|
||||||
/// Jump to a specific tab. Must handle the scenario that the tab
|
/// Jump to a specific tab. Must handle the scenario that the tab
|
||||||
/// value is invalid.
|
/// value is invalid.
|
||||||
goto_tab: GotoTab,
|
goto_tab: GotoTab,
|
||||||
@ -212,6 +215,7 @@ pub const Action = union(Key) {
|
|||||||
toggle_quick_terminal,
|
toggle_quick_terminal,
|
||||||
toggle_visibility,
|
toggle_visibility,
|
||||||
move_tab,
|
move_tab,
|
||||||
|
move_tab_to_new_window,
|
||||||
goto_tab,
|
goto_tab,
|
||||||
goto_split,
|
goto_split,
|
||||||
resize_split,
|
resize_split,
|
||||||
|
@ -214,6 +214,7 @@ pub const App = struct {
|
|||||||
.toggle_visibility,
|
.toggle_visibility,
|
||||||
.goto_tab,
|
.goto_tab,
|
||||||
.move_tab,
|
.move_tab,
|
||||||
|
.move_tab_to_new_window,
|
||||||
.inspector,
|
.inspector,
|
||||||
.render_inspector,
|
.render_inspector,
|
||||||
.quit_timer,
|
.quit_timer,
|
||||||
|
@ -457,6 +457,7 @@ pub fn performAction(
|
|||||||
.new_tab => try self.newTab(target),
|
.new_tab => try self.newTab(target),
|
||||||
.goto_tab => self.gotoTab(target, value),
|
.goto_tab => self.gotoTab(target, value),
|
||||||
.move_tab => self.moveTab(target, value),
|
.move_tab => self.moveTab(target, value),
|
||||||
|
.move_tab_to_new_window => self.moveTabToNewWindow(target),
|
||||||
.new_split => try self.newSplit(target, value),
|
.new_split => try self.newSplit(target, value),
|
||||||
.resize_split => self.resizeSplit(target, value),
|
.resize_split => self.resizeSplit(target, value),
|
||||||
.equalize_splits => self.equalizeSplits(target),
|
.equalize_splits => self.equalizeSplits(target),
|
||||||
@ -548,6 +549,23 @@ fn moveTab(_: *App, target: apprt.Target, move_tab: apprt.action.MoveTab) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn moveTabToNewWindow(_: *App, target: apprt.Target) void {
|
||||||
|
switch (target) {
|
||||||
|
.app => {},
|
||||||
|
.surface => |v| {
|
||||||
|
const window = v.rt_surface.container.window() orelse {
|
||||||
|
log.info(
|
||||||
|
"moveTabToNewWindow invalid for container={s}",
|
||||||
|
.{@tagName(v.rt_surface.container)},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.moveTabToNewWindow(v.rt_surface);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn newSplit(
|
fn newSplit(
|
||||||
self: *App,
|
self: *App,
|
||||||
target: apprt.Target,
|
target: apprt.Target,
|
||||||
@ -917,6 +935,7 @@ fn syncActionAccelerators(self: *App) !void {
|
|||||||
try self.syncActionAccelerator("win.split_down", .{ .new_split = .down });
|
try self.syncActionAccelerator("win.split_down", .{ .new_split = .down });
|
||||||
try self.syncActionAccelerator("win.split_left", .{ .new_split = .left });
|
try self.syncActionAccelerator("win.split_left", .{ .new_split = .left });
|
||||||
try self.syncActionAccelerator("win.split_up", .{ .new_split = .up });
|
try self.syncActionAccelerator("win.split_up", .{ .new_split = .up });
|
||||||
|
try self.syncActionAccelerator("win.move_tab_to_new_window", .{ .move_tab_to_new_window = {} });
|
||||||
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
|
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
|
||||||
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
|
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
|
||||||
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
|
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
|
||||||
@ -1561,7 +1580,13 @@ fn initContextMenu(self: *App) void {
|
|||||||
const menu = c.g_menu_new();
|
const menu = c.g_menu_new();
|
||||||
errdefer c.g_object_unref(menu);
|
errdefer c.g_object_unref(menu);
|
||||||
|
|
||||||
createContextMenuCopyPasteSection(menu, false);
|
{
|
||||||
|
const section = c.g_menu_new();
|
||||||
|
defer c.g_object_unref(section);
|
||||||
|
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||||
|
c.g_menu_append(section, "Copy", "win.copy");
|
||||||
|
c.g_menu_append(section, "Paste", "win.paste");
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const section = c.g_menu_new();
|
const section = c.g_menu_new();
|
||||||
@ -1569,6 +1594,7 @@ fn initContextMenu(self: *App) void {
|
|||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||||
c.g_menu_append(section, "Split Right", "win.split_right");
|
c.g_menu_append(section, "Split Right", "win.split_right");
|
||||||
c.g_menu_append(section, "Split Down", "win.split_down");
|
c.g_menu_append(section, "Split Down", "win.split_down");
|
||||||
|
c.g_menu_append(section, "Move Tab to New Window", "win.move_tab_to_new_window");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -1582,18 +1608,9 @@ fn initContextMenu(self: *App) void {
|
|||||||
self.context_menu = menu;
|
self.context_menu = menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createContextMenuCopyPasteSection(menu: ?*c.GMenu, has_selection: bool) void {
|
pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
|
||||||
const section = c.g_menu_new();
|
const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
|
||||||
defer c.g_object_unref(section);
|
c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
|
||||||
c.g_menu_prepend_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
// FIXME: Feels really hackish, but disabling sensitivity on this doesn't seems to work(?)
|
|
||||||
c.g_menu_append(section, "Copy", if (has_selection) "win.copy" else "noop");
|
|
||||||
c.g_menu_append(section, "Paste", "win.paste");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refreshContextMenu(self: *App, has_selection: bool) void {
|
|
||||||
c.g_menu_remove(self.context_menu, 0);
|
|
||||||
createContextMenuCopyPasteSection(self.context_menu, has_selection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isValidAppId(app_id: [:0]const u8) bool {
|
fn isValidAppId(app_id: [:0]const u8) bool {
|
||||||
|
@ -1195,7 +1195,7 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect);
|
c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect);
|
||||||
self.app.refreshContextMenu(self.core_surface.hasSelection());
|
self.app.refreshContextMenu(window.window, self.core_surface.hasSelection());
|
||||||
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
|
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
|||||||
|
|
||||||
// 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");
|
window.notebook.addTab(self, "Ghostty");
|
||||||
|
|
||||||
// Attach all events
|
// Attach all events
|
||||||
_ = 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);
|
||||||
|
@ -42,8 +42,7 @@ header: ?*c.GtkWidget,
|
|||||||
tab_overview: ?*c.GtkWidget,
|
tab_overview: ?*c.GtkWidget,
|
||||||
|
|
||||||
/// The notebook (tab grouping) for this window.
|
/// The notebook (tab grouping) for this window.
|
||||||
/// can be either c.GtkNotebook or c.AdwTabView.
|
notebook: *Notebook,
|
||||||
notebook: Notebook,
|
|
||||||
|
|
||||||
context_menu: *c.GtkWidget,
|
context_menu: *c.GtkWidget,
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ toast_overlay: ?*c.GtkWidget,
|
|||||||
/// See adwTabOverviewOpen for why we have this.
|
/// See adwTabOverviewOpen for why we have this.
|
||||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||||
|
|
||||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
pub fn create(alloc: Allocator, app: *App) std.mem.Allocator.Error!*Window {
|
||||||
// Allocate a fixed pointer for our window. We try to minimize
|
// Allocate a fixed pointer for our window. We try to minimize
|
||||||
// allocations but windows and other GUI requirements are so minimal
|
// allocations but windows and other GUI requirements are so minimal
|
||||||
// compared to the steady-state terminal operation so we use heap
|
// compared to the steady-state terminal operation so we use heap
|
||||||
@ -64,11 +63,11 @@ pub fn create(alloc: Allocator, app: *App) !*Window {
|
|||||||
// freed when the window is closed.
|
// freed when the window is closed.
|
||||||
var window = try alloc.create(Window);
|
var window = try alloc.create(Window);
|
||||||
errdefer alloc.destroy(window);
|
errdefer alloc.destroy(window);
|
||||||
try window.init(app);
|
try window.init(alloc, app);
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(self: *Window, app: *App) !void {
|
pub fn init(self: *Window, alloc: Allocator, app: *App) std.mem.Allocator.Error!void {
|
||||||
// Set up our own state
|
// Set up our own state
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
@ -226,7 +225,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup our notebook
|
// Setup our notebook
|
||||||
self.notebook = Notebook.create(self);
|
self.notebook = try Notebook.create(alloc, self);
|
||||||
|
|
||||||
// Setup our toast overlay if we have one
|
// Setup our toast overlay if we have one
|
||||||
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
|
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
|
||||||
@ -245,8 +244,8 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
// If we have a tab overview then we can set it on our notebook.
|
// If we have a tab overview then we can set it on our notebook.
|
||||||
if (self.tab_overview) |tab_overview| {
|
if (self.tab_overview) |tab_overview| {
|
||||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||||
assert(self.notebook == .adw_tab_view);
|
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.adw.tab_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
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)));
|
||||||
@ -280,7 +279,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?));
|
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?));
|
||||||
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
|
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
|
||||||
const tab_bar = c.adw_tab_bar_new();
|
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.adw.tab_view);
|
||||||
|
|
||||||
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||||
|
|
||||||
@ -323,8 +322,8 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (self.notebook) {
|
switch (self.notebook.*) {
|
||||||
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
.adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||||
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
|
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
|
||||||
// an AdwToolbarView.
|
// an AdwToolbarView.
|
||||||
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
|
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
|
||||||
@ -344,14 +343,14 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
@ptrCast(@alignCast(tab_bar)),
|
@ptrCast(@alignCast(tab_bar)),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
c.adw_tab_bar_set_view(tab_bar, tab_view);
|
c.adw_tab_bar_set_view(tab_bar, adw.tab_view);
|
||||||
|
|
||||||
if (!app.config.@"gtk-wide-tabs") {
|
if (!app.config.@"gtk-wide-tabs") {
|
||||||
c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.gtk_notebook => {},
|
.gtk => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// The box is our main child
|
// The box is our main child
|
||||||
@ -376,6 +375,7 @@ fn initActions(self: *Window) void {
|
|||||||
.{ "split_down", >kActionSplitDown },
|
.{ "split_down", >kActionSplitDown },
|
||||||
.{ "split_left", >kActionSplitLeft },
|
.{ "split_left", >kActionSplitLeft },
|
||||||
.{ "split_up", >kActionSplitUp },
|
.{ "split_up", >kActionSplitUp },
|
||||||
|
.{ "move_tab_to_new_window", >kActionMoveTabToNewWindow },
|
||||||
.{ "toggle_inspector", >kActionToggleInspector },
|
.{ "toggle_inspector", >kActionToggleInspector },
|
||||||
.{ "copy", >kActionCopy },
|
.{ "copy", >kActionCopy },
|
||||||
.{ "paste", >kActionPaste },
|
.{ "paste", >kActionPaste },
|
||||||
@ -403,6 +403,8 @@ pub fn deinit(self: *Window) void {
|
|||||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||||
_ = c.g_source_remove(timer);
|
_ = c.g_source_remove(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.app.core_app.alloc.destroy(self.notebook);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this window should use an Adwaita window.
|
/// Returns true if this window should use an Adwaita window.
|
||||||
@ -461,6 +463,15 @@ pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
|
|||||||
self.notebook.moveTab(tab, position);
|
self.notebook.moveTab(tab, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move the current tab for a surface to a new window.
|
||||||
|
pub fn moveTabToNewWindow(self: *Window, surface: *Surface) void {
|
||||||
|
const tab = surface.container.tab() orelse {
|
||||||
|
log.info("surface is not attached to a tab bar, cannot navigate", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.notebook.moveTabToNewWindow(tab);
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 = self.notebook.nPages() -| 1;
|
const max = self.notebook.nPages() -| 1;
|
||||||
@ -552,14 +563,14 @@ fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwT
|
|||||||
const alloc = self.app.core_app.alloc;
|
const alloc = self.app.core_app.alloc;
|
||||||
const surface = self.actionSurface();
|
const surface = self.actionSurface();
|
||||||
const tab = Tab.create(alloc, self, surface) catch return null;
|
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.adw.tab_view, @ptrCast(@alignCast(tab.box)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adwTabOverviewOpen(
|
fn adwTabOverviewOpen(
|
||||||
object: *c.GObject,
|
object: *c.GObject,
|
||||||
_: *c.GParamSpec,
|
_: *c.GParamSpec,
|
||||||
ud: ?*anyopaque,
|
ud: ?*anyopaque,
|
||||||
) void {
|
) callconv(.C) void {
|
||||||
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(object));
|
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(object));
|
||||||
|
|
||||||
// We only care about when the tab overview is closed.
|
// We only care about when the tab overview is closed.
|
||||||
@ -852,6 +863,19 @@ fn gtkActionSplitUp(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gtkActionMoveTabToNewWindow(
|
||||||
|
_: *c.GSimpleAction,
|
||||||
|
_: *c.GVariant,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
const surface = self.actionSurface() orelse return;
|
||||||
|
_ = surface.performBindingAction(.{ .move_tab_to_new_window = {} }) catch |err| {
|
||||||
|
log.warn("error performing binding action error={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn gtkActionToggleInspector(
|
fn gtkActionToggleInspector(
|
||||||
_: *c.GSimpleAction,
|
_: *c.GSimpleAction,
|
||||||
_: *c.GVariant,
|
_: *c.GVariant,
|
||||||
|
@ -4,161 +4,83 @@ const c = @import("c.zig").c;
|
|||||||
|
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
const Tab = @import("Tab.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 adwaita = @import("adwaita.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
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
|
/// An abstraction over the GTK notebook and Adwaita tab view to manage
|
||||||
/// all the terminal tabs in a window.
|
/// all the terminal tabs in a window.
|
||||||
pub const Notebook = union(enum) {
|
pub const Notebook = union(enum) {
|
||||||
adw_tab_view: *AdwTabView,
|
adw: NotebookAdw,
|
||||||
gtk_notebook: *c.GtkNotebook,
|
gtk: NotebookGtk,
|
||||||
|
|
||||||
pub fn create(window: *Window) Notebook {
|
pub fn create(alloc: std.mem.Allocator, window: *Window) std.mem.Allocator.Error!*Notebook {
|
||||||
|
// Allocate a fixed pointer for our notebook. We try to minimize
|
||||||
|
// allocations but windows and other GUI requirements are so minimal
|
||||||
|
// compared to the steady-state terminal operation so we use heap
|
||||||
|
// allocation for this.
|
||||||
|
//
|
||||||
|
// The allocation is owned by the GtkWindow created. It will be
|
||||||
|
// freed when the window is closed.
|
||||||
|
const notebook = try alloc.create(Notebook);
|
||||||
|
errdefer alloc.destroy(notebook);
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
if (adwaita.enabled(&app.config)) return initAdw(window);
|
if (adwaita.enabled(&app.config)) {
|
||||||
return initGtk(window);
|
NotebookAdw.init(notebook, window);
|
||||||
}
|
return notebook;
|
||||||
|
|
||||||
fn initGtk(window: *Window) 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);
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initAdw(window: *Window) Notebook {
|
|
||||||
const app = window.app;
|
|
||||||
assert(adwaita.enabled(&app.config));
|
|
||||||
|
|
||||||
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
NotebookGtk.init(notebook, window);
|
||||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
return notebook;
|
||||||
_ = 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 asWidget(self: Notebook) *c.GtkWidget {
|
pub fn asWidget(self: *Notebook) *c.GtkWidget {
|
||||||
return switch (self) {
|
return switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| @ptrCast(@alignCast(tab_view)),
|
.adw => |*adw| adw.asWidget(),
|
||||||
.gtk_notebook => |notebook| @ptrCast(@alignCast(notebook)),
|
.gtk => |*gtk| gtk.asWidget(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nPages(self: Notebook) c_int {
|
pub fn nPages(self: *Notebook) c_int {
|
||||||
return switch (self) {
|
return switch (self.*) {
|
||||||
.gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook),
|
.adw => |*adw| adw.nPages(),
|
||||||
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0))
|
.gtk => |*gtk| gtk.nPages(),
|
||||||
c.adw_tab_view_get_n_pages(tab_view)
|
|
||||||
else
|
|
||||||
unreachable,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the index of the currently selected page.
|
/// Returns the index of the currently selected page.
|
||||||
/// Returns null if the notebook has no pages.
|
/// Returns null if the notebook has no pages.
|
||||||
fn currentPage(self: Notebook) ?c_int {
|
fn currentPage(self: *Notebook) ?c_int {
|
||||||
switch (self) {
|
return switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| {
|
.adw => |*adw| adw.currentPage(),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.currentPage(),
|
||||||
const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null;
|
};
|
||||||
return c.adw_tab_view_get_page_position(tab_view, page);
|
|
||||||
},
|
|
||||||
|
|
||||||
.gtk_notebook => |notebook| {
|
|
||||||
const current = c.gtk_notebook_get_current_page(notebook);
|
|
||||||
return if (current == -1) null else current;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the currently selected tab or null if there are none.
|
/// Returns the currently selected tab or null if there are none.
|
||||||
pub fn currentTab(self: Notebook) ?*Tab {
|
pub fn currentTab(self: *Notebook) ?*Tab {
|
||||||
const child = switch (self) {
|
return switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| child: {
|
.adw => |*adw| adw.currentTab(),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.currentTab(),
|
||||||
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() orelse 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 {
|
pub fn gotoNthTab(self: *Notebook, position: c_int) void {
|
||||||
switch (self) {
|
switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| {
|
.adw => |*adw| adw.gotoNthTab(position),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.gotoNthTab(position),
|
||||||
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 {
|
pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
|
||||||
return switch (self) {
|
return switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| page_idx: {
|
.adw => |*adw| adw.getTabPosition(tab),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.getTabPosition(tab),
|
||||||
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 {
|
pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) void {
|
||||||
const page_idx = self.getTabPosition(tab) orelse return;
|
const page_idx = self.getTabPosition(tab) orelse return;
|
||||||
|
|
||||||
// The next index is the previous or we wrap around.
|
// The next index is the previous or we wrap around.
|
||||||
@ -173,7 +95,7 @@ pub const Notebook = union(enum) {
|
|||||||
self.gotoNthTab(next_idx);
|
self.gotoNthTab(next_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gotoNextTab(self: Notebook, tab: *Tab) void {
|
pub fn gotoNextTab(self: *Notebook, tab: *Tab) void {
|
||||||
const page_idx = self.getTabPosition(tab) orelse return;
|
const page_idx = self.getTabPosition(tab) orelse return;
|
||||||
|
|
||||||
const max = self.nPages() -| 1;
|
const max = self.nPages() -| 1;
|
||||||
@ -183,7 +105,7 @@ pub const Notebook = union(enum) {
|
|||||||
self.gotoNthTab(next_idx);
|
self.gotoNthTab(next_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn moveTab(self: Notebook, tab: *Tab, position: c_int) void {
|
pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
|
||||||
const page_idx = self.getTabPosition(tab) orelse return;
|
const page_idx = self.getTabPosition(tab) orelse return;
|
||||||
|
|
||||||
const max = self.nPages() -| 1;
|
const max = self.nPages() -| 1;
|
||||||
@ -199,42 +121,35 @@ pub const Notebook = union(enum) {
|
|||||||
self.reorderPage(tab, new_position);
|
self.reorderPage(tab, new_position);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reorderPage(self: Notebook, tab: *Tab, position: c_int) void {
|
pub fn moveTabToNewWindow(self: *Notebook, tab: *Tab) void {
|
||||||
switch (self) {
|
switch (self.*) {
|
||||||
.gtk_notebook => |notebook| {
|
.adw => |*adw| adw.moveTabToNewWindow(tab),
|
||||||
c.gtk_notebook_reorder_child(notebook, @ptrCast(tab.box), position);
|
.gtk => log.warn("move_tab_to_new_window is not implemented for non-Adwaita notebooks", .{}),
|
||||||
},
|
|
||||||
.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_view_reorder_page(tab_view, page, position);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void {
|
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
|
||||||
switch (self) {
|
switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| {
|
.adw => |*adw| adw.reorderPage(tab, position),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.reorderPage(tab, position),
|
||||||
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 setTabTooltip(self: Notebook, tab: *Tab, tooltip: [:0]const u8) void {
|
pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||||
switch (self) {
|
switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| {
|
.adw => |*adw| adw.setTabLabel(tab, title),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.setTabLabel(tab, title),
|
||||||
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
|
|
||||||
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
|
|
||||||
},
|
|
||||||
.gtk_notebook => c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn newTabInsertPosition(self: Notebook, tab: *Tab) c_int {
|
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();
|
const numPages = self.nPages();
|
||||||
return switch (tab.window.app.config.@"window-new-tab-position") {
|
return switch (tab.window.app.config.@"window-new-tab-position") {
|
||||||
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
||||||
@ -243,249 +158,23 @@ pub const Notebook = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new tab with the given title to the notebook.
|
/// Adds a new tab with the given title to the notebook.
|
||||||
pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !void {
|
pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
|
||||||
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
|
const position = self.newTabInsertPosition(tab);
|
||||||
switch (self) {
|
switch (self.*) {
|
||||||
.adw_tab_view => |tab_view| {
|
.adw => |*adw| adw.addTab(tab, position, title),
|
||||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
.gtk => |*gtk| gtk.addTab(tab, position, title),
|
||||||
|
|
||||||
const page = c.adw_tab_view_insert(tab_view, box_widget, self.newTabInsertPosition(tab));
|
|
||||||
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 page_idx = c.gtk_notebook_insert_page(
|
|
||||||
notebook,
|
|
||||||
box_widget,
|
|
||||||
label_box_widget,
|
|
||||||
self.newTabInsertPosition(tab),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Tab settings
|
|
||||||
c.gtk_notebook_set_tab_reorderable(notebook, box_widget, 1);
|
|
||||||
c.gtk_notebook_set_tab_detachable(notebook, box_widget, 1);
|
|
||||||
|
|
||||||
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 {
|
pub fn closeTab(self: *Notebook, tab: *Tab) void {
|
||||||
const window = tab.window;
|
switch (self.*) {
|
||||||
switch (self) {
|
.adw => |*adw| adw.closeTab(tab),
|
||||||
.adw_tab_view => |tab_view| {
|
.gtk => |*gtk| gtk.closeTab(tab),
|
||||||
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();
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
pub fn createWindow(currentWindow: *Window) !*Window {
|
||||||
_: *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 alloc = currentWindow.app.core_app.alloc;
|
||||||
const app = currentWindow.app;
|
const app = currentWindow.app;
|
||||||
|
|
||||||
|
276
src/apprt/gtk/notebook_adw.zig
Normal file
276
src/apprt/gtk/notebook_adw.zig
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
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 window
|
||||||
|
window: *Window,
|
||||||
|
|
||||||
|
/// the tab view
|
||||||
|
tab_view: *AdwTabView,
|
||||||
|
|
||||||
|
/// the last tab to have the context menu shown
|
||||||
|
last_tab: ?*Tab,
|
||||||
|
|
||||||
|
pub fn init(notebook: *Notebook, window: *Window) void {
|
||||||
|
const app = window.app;
|
||||||
|
assert(adwaita.enabled(&app.config));
|
||||||
|
|
||||||
|
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
||||||
|
|
||||||
|
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 = .{
|
||||||
|
.window = window,
|
||||||
|
.tab_view = tab_view,
|
||||||
|
.last_tab = null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const self = ¬ebook.adw;
|
||||||
|
self.initContextMenu(window);
|
||||||
|
|
||||||
|
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwTabViewPageAttached), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(tab_view, "page-reordered", c.G_CALLBACK(&adwTabViewPageAttached), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(tab_view, "setup-menu", c.G_CALLBACK(&adwTabViewSetupMenu), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initContextMenu(self: *NotebookAdw, window: *Window) void {
|
||||||
|
const menu = c.g_menu_new();
|
||||||
|
errdefer c.g_object_unref(menu);
|
||||||
|
|
||||||
|
{
|
||||||
|
const section = c.g_menu_new();
|
||||||
|
defer c.g_object_unref(section);
|
||||||
|
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||||
|
// The set of menu items. Each menu item has (in order):
|
||||||
|
// [0] The action name
|
||||||
|
// [1] The menu name
|
||||||
|
// [2] The callback function
|
||||||
|
const menu_items = .{
|
||||||
|
.{ "close-tab", "Close Tab", &adwTabViewCloseTab },
|
||||||
|
.{ "move-tab-to-new-window", "Move Tab to New Window", &adwTabViewMoveTabToNewWindow },
|
||||||
|
};
|
||||||
|
|
||||||
|
inline for (menu_items) |menu_item| {
|
||||||
|
var buf: [48]u8 = undefined;
|
||||||
|
|
||||||
|
const action_name = std.fmt.bufPrintZ(
|
||||||
|
&buf,
|
||||||
|
"{s}-{x:8>0}",
|
||||||
|
.{ menu_item[0], @intFromPtr(self) },
|
||||||
|
) catch unreachable;
|
||||||
|
|
||||||
|
const action = c.g_simple_action_new(action_name, null);
|
||||||
|
defer c.g_object_unref(action);
|
||||||
|
_ = c.g_signal_connect_data(
|
||||||
|
action,
|
||||||
|
"activate",
|
||||||
|
c.G_CALLBACK(menu_item[2]),
|
||||||
|
self,
|
||||||
|
null,
|
||||||
|
c.G_CONNECT_DEFAULT,
|
||||||
|
);
|
||||||
|
c.g_action_map_add_action(@ptrCast(window.window), @ptrCast(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (menu_items) |menu_item| {
|
||||||
|
var buf: [48]u8 = undefined;
|
||||||
|
const action_name = std.fmt.bufPrintZ(
|
||||||
|
&buf,
|
||||||
|
"win.{s}-{x:8>0}",
|
||||||
|
.{ menu_item[0], @intFromPtr(self) },
|
||||||
|
) catch unreachable;
|
||||||
|
|
||||||
|
c.g_menu_append(section, menu_item[1], action_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.adw_tab_view_set_menu_model(self.tab_view, @ptrCast(@alignCast(menu)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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(self.window.window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moveTabToNewWindow(self: *NotebookAdw, tab: *Tab) void {
|
||||||
|
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse {
|
||||||
|
log.err("tab is not part of this notebook", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const other_window = createWindow(self.window) catch {
|
||||||
|
log.err("unable to create window", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
switch (other_window.notebook.*) {
|
||||||
|
.adw => |*other| {
|
||||||
|
c.adw_tab_view_transfer_page(self.tab_view, page, other.tab_view, 0);
|
||||||
|
other_window.focusCurrentTab();
|
||||||
|
},
|
||||||
|
.gtk => {
|
||||||
|
log.err("expecting an Adwaita notebook!", .{});
|
||||||
|
c.gtk_window_destroy(other_window.window);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.nPages() == 0) {
|
||||||
|
c.gtk_window_destroy(self.window.window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn adwTabViewPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const self: *NotebookAdw = @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.window;
|
||||||
|
|
||||||
|
self.window.focusCurrentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwTabViewCreateWindow(
|
||||||
|
_: *AdwTabView,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) ?*AdwTabView {
|
||||||
|
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
|
||||||
|
const window = createWindow(self.window) 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 self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
|
||||||
|
const page = c.adw_tab_view_get_selected_page(self.window.notebook.adw.tab_view) orelse return;
|
||||||
|
const title = c.adw_tab_page_get_title(page);
|
||||||
|
c.gtk_window_set_title(self.window.window, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwTabViewSetupMenu(_: *AdwTabView, page: *AdwTabPage, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
|
||||||
|
self.last_tab = null;
|
||||||
|
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
|
||||||
|
self.last_tab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwTabViewCloseTab(_: *c.GSimpleAction, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
|
||||||
|
self.closeTab(self.last_tab orelse return);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adwTabViewMoveTabToNewWindow(_: *c.GSimpleAction, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const self: *NotebookAdw = @ptrCast(@alignCast(ud.?));
|
||||||
|
self.moveTabToNewWindow(self.last_tab orelse return);
|
||||||
|
}
|
282
src/apprt/gtk/notebook_gtk.zig
Normal file
282
src/apprt/gtk/notebook_gtk.zig
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
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, window: *Window) void {
|
||||||
|
const app = window.app;
|
||||||
|
|
||||||
|
// Create a notebook to hold our tabs.
|
||||||
|
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
|
||||||
|
const gtk_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(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(&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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
c.gtk_window_set_title(window.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: *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;
|
||||||
|
}
|
@ -306,6 +306,10 @@ pub const Action = union(enum) {
|
|||||||
/// If the new position is out of bounds, it wraps around cyclically within the tab range.
|
/// If the new position is out of bounds, it wraps around cyclically within the tab range.
|
||||||
move_tab: isize,
|
move_tab: isize,
|
||||||
|
|
||||||
|
/// Move tab to a new window, where it will become the first and only tab in
|
||||||
|
/// that window.
|
||||||
|
move_tab_to_new_window: void,
|
||||||
|
|
||||||
/// Toggle the tab overview.
|
/// Toggle the tab overview.
|
||||||
/// This only works with libadwaita enabled currently.
|
/// This only works with libadwaita enabled currently.
|
||||||
toggle_tab_overview: void,
|
toggle_tab_overview: void,
|
||||||
@ -665,6 +669,7 @@ pub const Action = union(enum) {
|
|||||||
.last_tab,
|
.last_tab,
|
||||||
.goto_tab,
|
.goto_tab,
|
||||||
.move_tab,
|
.move_tab,
|
||||||
|
.move_tab_to_new_window,
|
||||||
.toggle_tab_overview,
|
.toggle_tab_overview,
|
||||||
.new_split,
|
.new_split,
|
||||||
.goto_split,
|
.goto_split,
|
||||||
|
Reference in New Issue
Block a user