Files
ghostty/src/apprt/gtk/class/window.zig
Mitchell Hashimoto 24f883904d gtk: fix duplicate signal handlers (#9001)
Signal handlers are connected to surface objects in two spots - when a
tab is added to a page and when the split tree changes. This resulted in
duplicate signal handlers being added for each surface. This was most
noticeable when copying the selection to the clipboard - you would see
two "Copied to clipboard" toasts. Ensure that there is only one signal
handler by removing any old ones before adding the new ones.
2025-10-06 09:04:44 -07:00

2003 lines
65 KiB
Zig

const std = @import("std");
const build_config = @import("../../../build_config.zig");
const assert = std.debug.assert;
const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const configpkg = @import("../../../config.zig");
const TitlebarStyle = configpkg.Config.GtkTitlebarStyle;
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
const gtk_version = @import("../gtk_version.zig");
const adw_version = @import("../adw_version.zig");
const gresource = @import("../build/gresource.zig");
const winprotopkg = @import("../winproto.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const SplitTree = @import("split_tree.zig").SplitTree;
const Surface = @import("surface.zig").Surface;
const Tab = @import("tab.zig").Tab;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const CommandPalette = @import("command_palette.zig").CommandPalette;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_window);
pub const Window = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.ApplicationWindow;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyWindow",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
/// The active surface is the focus that should be receiving all
/// surface-targeted actions. This is usually the focused surface,
/// but may also not be focused if the user has selected a non-surface
/// widget.
pub const @"active-surface" = struct {
pub const name = "active-surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = gobject.ext.typedAccessor(
Self,
?*Surface,
.{
.getter = Self.getActiveSurface,
},
),
},
);
};
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Config,
.{
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
pub const debug = struct {
pub const name = "debug";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = build_config.is_debug,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = struct {
pub fn getter(_: *Self) bool {
return build_config.is_debug;
}
}.getter,
}),
},
);
};
pub const @"titlebar-style" = struct {
pub const name = "titlebar-style";
const impl = gobject.ext.defineProperty(
name,
Self,
TitlebarStyle,
.{
.default = .native,
.accessor = gobject.ext.typedAccessor(
Self,
TitlebarStyle,
.{
.getter = Self.getTitlebarStyle,
},
),
},
);
};
pub const @"headerbar-visible" = struct {
pub const name = "headerbar-visible";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getHeaderbarVisible,
}),
},
);
};
pub const @"quick-terminal" = struct {
pub const name = "quick-terminal";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = true,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"quick_terminal",
),
},
);
};
pub const @"tabs-autohide" = struct {
pub const name = "tabs-autohide";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getTabsAutohide,
}),
},
);
};
pub const @"tabs-wide" = struct {
pub const name = "tabs-wide";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getTabsWide,
}),
},
);
};
pub const @"tabs-visible" = struct {
pub const name = "tabs-visible";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getTabsVisible,
}),
},
);
};
pub const @"toolbar-style" = struct {
pub const name = "toolbar-style";
const impl = gobject.ext.defineProperty(
name,
Self,
adw.ToolbarStyle,
.{
.default = .raised,
.accessor = gobject.ext.typedAccessor(
Self,
adw.ToolbarStyle,
.{
.getter = Self.getToolbarStyle,
},
),
},
);
};
};
const Private = struct {
/// Whether this window is a quick terminal. If it is then it
/// behaves slightly differently under certain scenarios.
quick_terminal: bool = false,
/// The window decoration override. If this is not set then we'll
/// inherit whatever the config has. This allows overriding the
/// config on a per-window basis.
window_decoration: ?configpkg.WindowDecoration = null,
/// Binding group for our active tab.
tab_bindings: *gobject.BindingGroup,
/// The configuration that this surface is using.
config: ?*Config = null,
/// State and logic for windowing protocol for a window.
winproto: winprotopkg.Window,
/// Kind of hacky to have this but this lets us know if we've
/// initialized any single surface yet. We need this because we
/// gate default size on this so that we don't resize the window
/// after surfaces already exist.
///
/// I think long term we can probably get rid of this by implementing
/// a property or method that gets us all the surfaces in all the
/// tabs and checking if we have zero or one that isn't initialized.
///
/// For now, this logic is more similar to our legacy GTK side.
surface_init: bool = false,
/// See tabOverviewOpen for why we have this.
tab_overview_focus_timer: ?c_uint = null,
/// A weak reference to a command palette.
command_palette: WeakRef(CommandPalette) = .empty,
// Template bindings
tab_overview: *adw.TabOverview,
tab_bar: *adw.TabBar,
tab_view: *adw.TabView,
toolbar: *adw.ToolbarView,
toast_overlay: *adw.ToastOverlay,
pub var offset: c_int = 0;
};
pub fn new(app: *Application) *Self {
return gobject.ext.newInstance(Self, .{
.application = app,
});
}
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// If our configuration is null then we get the configuration
// from the application.
const priv = self.private();
if (priv.config == null) {
const app = Application.default();
priv.config = app.getConfig();
}
// We initialize our windowing protocol to none because we can't
// actually initialize this until we get realized.
priv.winproto = .none;
// Add our dev CSS class if we're in debug mode.
if (comptime build_config.is_debug) {
self.as(gtk.Widget).addCssClass("devel");
}
// Setup our tab binding group. This ensures certain properties
// are only synced from the currently active tab.
priv.tab_bindings = gobject.BindingGroup.new();
priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{});
// Set our window icon. We can't set this in the blueprint file
// because its dependent on the build config.
self.as(gtk.Window).setIconName(build_config.bundle_id);
// Initialize our actions
self.initActionMap();
// Start states based on config.
if (priv.config) |config_obj| {
const config = config_obj.get();
if (config.maximize) self.as(gtk.Window).maximize();
if (config.fullscreen) self.as(gtk.Window).fullscreen();
// If we have an explicit title set, we set that immediately
// so that any applications inspecting the window states see
// an immediate title set when the window appears, rather than
// waiting possibly a few event loop ticks for it to sync from
// the surface.
if (config.title) |v| self.as(gtk.Window).setTitle(v);
}
// We always sync our appearance at the end because loading our
// config and such can affect our bindings which are setup initially
// in initTemplate.
self.syncAppearance();
// We need to do this so that the title initializes properly,
// I think because its a dynamic getter.
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
/// Setup our action map.
fn initActionMap(self: *Self) void {
const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
defer s_variant_type.free();
const actions = [_]ext.actions.Action(Self){
.init("about", actionAbout, null),
.init("close", actionClose, null),
.init("close-tab", actionCloseTab, s_variant_type),
.init("new-tab", actionNewTab, null),
.init("new-window", actionNewWindow, null),
.init("ring-bell", actionRingBell, null),
.init("split-right", actionSplitRight, null),
.init("split-left", actionSplitLeft, null),
.init("split-up", actionSplitUp, null),
.init("split-down", actionSplitDown, null),
.init("copy", actionCopy, null),
.init("paste", actionPaste, null),
.init("reset", actionReset, null),
.init("clear", actionClear, null),
// TODO: accept the surface that toggled the command palette
.init("toggle-command-palette", actionToggleCommandPalette, null),
.init("toggle-inspector", actionToggleInspector, null),
};
ext.actions.add(Self, self, &actions);
}
/// Winproto backend for this window.
pub fn winproto(self: *Self) *winprotopkg.Window {
return &self.private().winproto;
}
/// Create a new tab with the given parent. The tab will be inserted
/// at the position dictated by the `window-new-tab-position` config.
/// The new tab will be selected.
pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
_ = self.newTabPage(parent_);
}
fn newTabPage(self: *Self, parent_: ?*CoreSurface) *adw.TabPage {
const priv = self.private();
const tab_view = priv.tab_view;
// Create our new tab object
const tab = gobject.ext.newInstance(Tab, .{
.config = priv.config,
});
if (parent_) |p| tab.setParent(p);
// Get the position that we should insert the new tab at.
const config = if (priv.config) |v| v.get() else {
// If we don't have a config we just append it at the end.
// This should never happen.
return tab_view.append(tab.as(gtk.Widget));
};
const position = switch (config.@"window-new-tab-position") {
.current => current: {
const selected = tab_view.getSelectedPage() orelse
break :current tab_view.getNPages();
const current = tab_view.getPagePosition(selected);
break :current current + 1;
},
.end => tab_view.getNPages(),
};
// Add the page and select it
const page = tab_view.insert(tab.as(gtk.Widget), position);
tab_view.setSelectedPage(page);
// Create some property bindings
_ = tab.as(gobject.Object).bindProperty(
"title",
page.as(gobject.Object),
"title",
.{ .sync_create = true },
);
_ = tab.as(gobject.Object).bindProperty(
"tooltip",
page.as(gobject.Object),
"tooltip",
.{ .sync_create = true },
);
// Bind signals
const split_tree = tab.getSplitTree();
_ = SplitTree.signals.changed.connect(
split_tree,
*Self,
tabSplitTreeChanged,
self,
.{},
);
// Run an initial notification for the surface tree so we can setup
// initial state.
tabSplitTreeChanged(
split_tree,
null,
split_tree.getTree(),
self,
);
return page;
}
pub const SelectTab = union(enum) {
previous,
next,
last,
n: usize,
};
/// Select the tab as requested. Returns true if the tab selection
/// changed.
pub fn selectTab(self: *Self, n: SelectTab) bool {
const priv = self.private();
const tab_view = priv.tab_view;
// Get our current tab numeric position
const selected = tab_view.getSelectedPage() orelse return false;
const current = tab_view.getPagePosition(selected);
// Get our total
const total = tab_view.getNPages();
const goto: c_int = switch (n) {
.previous => if (current > 0)
current - 1
else
total - 1,
.next => if (current < total - 1)
current + 1
else
0,
.last => total - 1,
.n => |v| n: {
// 1-indexed
if (v == 0) return false;
const n_int = std.math.cast(
c_int,
v,
) orelse return false;
break :n @min(n_int - 1, total - 1);
},
};
assert(goto >= 0);
assert(goto < total);
// If our target is the same as our current then we do nothing.
if (goto == current) return false;
// Add the page and select it
const page = tab_view.getNthPage(goto);
tab_view.setSelectedPage(page);
return true;
}
/// Move the tab containing the given surface by the given amount.
/// Returns if this affected any tab positioning.
pub fn moveTab(
self: *Self,
surface: *Surface,
amount: isize,
) bool {
const priv = self.private();
const tab_view = priv.tab_view;
// If we have one tab we never move.
const total = tab_view.getNPages();
if (total == 1) return false;
// Get the tab that contains the given surface.
const tab = ext.getAncestor(
Tab,
surface.as(gtk.Widget),
) orelse return false;
// Get the page position that contains the tab.
const page = tab_view.getPage(tab.as(gtk.Widget));
const pos = tab_view.getPagePosition(page);
// Move it
const desired_pos: c_int = desired: {
const initial: c_int = @intCast(pos + amount);
const max = total - 1;
break :desired if (initial < 0)
max + initial + 1
else if (initial > max)
initial - max - 1
else
initial;
};
assert(desired_pos >= 0);
assert(desired_pos < total);
return tab_view.reorderPage(page, desired_pos) != 0;
}
pub fn toggleTabOverview(self: *Self) void {
const priv = self.private();
const tab_overview = priv.tab_overview;
const is_open = tab_overview.getOpen() != 0;
tab_overview.setOpen(@intFromBool(!is_open));
}
/// Toggle the visible property.
pub fn toggleVisibility(self: *Self) void {
const widget = self.as(gtk.Widget);
widget.setVisible(@intFromBool(widget.isVisible() == 0));
}
/// Updates various appearance properties. This should always be safe
/// to call multiple times. This should be called whenever a change
/// happens that might affect how the window appears (config change,
/// fullscreen, etc.).
fn syncAppearance(self: *Self) void {
const priv = self.private();
const widget = self.as(gtk.Widget);
// Toggle style classes based on whether we're using CSDs or SSDs.
//
// These classes are defined in the gtk.Window documentation:
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes.
{
// Reset all style classes first
inline for (&.{
"ssd",
"csd",
"solid-csd",
"no-border-radius",
}) |class|
widget.removeCssClass(class);
const csd_enabled = priv.winproto.clientSideDecorationEnabled();
self.as(gtk.Window).setDecorated(@intFromBool(csd_enabled));
if (csd_enabled) {
const display = widget.getDisplay();
// We do the exact same check GTK is doing internally and toggle
// either the `csd` or `solid-csd` style, based on whether the user's
// window manager is deemed _non-compositing_.
//
// In practice this only impacts users of traditional X11 window
// managers (e.g. i3, dwm, awesomewm, etc.) and not X11 desktop
// environments or Wayland compositors/DEs.
if (display.isRgba() != 0 and display.isComposited() != 0) {
widget.addCssClass("csd");
} else {
widget.addCssClass("solid-csd");
}
} else {
widget.addCssClass("ssd");
// Fix any artifacting that may occur in window corners.
widget.addCssClass("no-border-radius");
}
}
// Trigger all our dynamic properties that depend on the config.
inline for (&.{
"headerbar-visible",
"tabs-autohide",
"tabs-visible",
"tabs-wide",
"toolbar-style",
"titlebar-style",
}) |key| {
self.as(gobject.Object).notifyByPspec(
@field(properties, key).impl.param_spec,
);
}
// Remainder uses the config
const config = if (priv.config) |v| v.get() else return;
// Only add a solid background if we're opaque.
self.toggleCssClass(
"background",
config.@"background-opacity" >= 1,
);
// Apply class to color headerbar if window-theme is set to `ghostty` and
// GTK version is before 4.16. The conditional is because above 4.16
// we use GTK CSS color variables.
self.toggleCssClass(
"window-theme-ghostty",
!gtk_version.atLeast(4, 16, 0) and
config.@"window-theme" == .ghostty,
);
// Move the tab bar to the proper location.
priv.toolbar.remove(priv.tab_bar.as(gtk.Widget));
switch (config.@"gtk-tabs-location") {
.top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)),
.bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)),
}
// Do our window-protocol specific appearance sync.
priv.winproto.syncAppearance() catch |err| {
log.warn("failed to sync winproto appearance error={}", .{err});
};
}
/// Sync the state of any actions on this window.
fn syncActions(self: *Self) void {
const has_selection = selection: {
const surface = self.getActiveSurface() orelse
break :selection false;
const core_surface = surface.core() orelse
break :selection false;
break :selection core_surface.hasSelection();
};
const action_map: *gio.ActionMap = gobject.ext.cast(
gio.ActionMap,
self,
) orelse return;
const action: *gio.SimpleAction = gobject.ext.cast(
gio.SimpleAction,
action_map.lookupAction("copy") orelse return,
) orelse return;
action.setEnabled(@intFromBool(has_selection));
}
fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
const widget = self.as(gtk.Widget);
if (value)
widget.addCssClass(class.ptr)
else
widget.removeCssClass(class.ptr);
}
/// Perform a binding action on the window's active surface.
fn performBindingAction(
self: *Self,
action: input.Binding.Action,
) void {
const surface = self.getActiveSurface() orelse return;
const core_surface = surface.core() orelse return;
_ = core_surface.performBindingAction(action) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
}
/// Queue a simple text-based toast. All text-based toasts share the
/// same timeout for consistency.
///
// This is not `pub` because we should be using signals emitted by
// other widgets to trigger our toasts. Other objects should not
// trigger toasts directly.
fn addToast(self: *Self, title: [*:0]const u8) void {
const toast = adw.Toast.new(title);
toast.setTimeout(3);
self.private().toast_overlay.addToast(toast);
}
fn connectSurfaceHandlers(
self: *Self,
tree: *const Surface.Tree,
) void {
const priv = self.private();
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
// Before adding any new signal handlers, disconnect any that we may
// have added before. Otherwise we may get multiple handlers for the
// same signal.
_ = gobject.signalHandlersDisconnectMatched(
surface.as(gobject.Object),
.{ .data = true },
0,
0,
null,
null,
self,
);
_ = Surface.signals.@"present-request".connect(
surface,
*Self,
surfacePresentRequest,
self,
.{},
);
_ = Surface.signals.@"clipboard-write".connect(
surface,
*Self,
surfaceClipboardWrite,
self,
.{},
);
_ = Surface.signals.menu.connect(
surface,
*Self,
surfaceMenu,
self,
.{},
);
_ = Surface.signals.@"toggle-fullscreen".connect(
surface,
*Self,
surfaceToggleFullscreen,
self,
.{},
);
_ = Surface.signals.@"toggle-maximize".connect(
surface,
*Self,
surfaceToggleMaximize,
self,
.{},
);
// If we've never had a surface initialize yet, then we register
// this signal. Its theoretically possible to launch multiple surfaces
// before init so we could register this on multiple and that is not
// a problem because we'll check the flag again in each handler.
if (!priv.surface_init) {
_ = Surface.signals.init.connect(
surface,
*Self,
surfaceInit,
self,
.{},
);
}
}
}
/// Disconnect all the surface handlers for the given tree. This should
/// be called whenever a tree is no longer present in the window, e.g.
/// when a tab is detached or the tree changes.
fn disconnectSurfaceHandlers(
self: *Self,
tree: *const Surface.Tree,
) void {
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
_ = gobject.signalHandlersDisconnectMatched(
surface.as(gobject.Object),
.{ .data = true },
0,
0,
null,
null,
self,
);
}
}
//---------------------------------------------------------------
// Properties
/// Whether this terminal is a quick terminal or not.
pub fn isQuickTerminal(self: *Self) bool {
return self.private().quick_terminal;
}
/// Get the currently active surface. See the "active-surface" property.
/// This does not ref the value.
fn getActiveSurface(self: *Self) ?*Surface {
const tab = self.getSelectedTab() orelse return null;
return tab.getActiveSurface();
}
/// Returns the configuration for this window. The reference count
/// is not increased.
pub fn getConfig(self: *Self) ?*Config {
return self.private().config;
}
/// Get the current window decoration value for this window.
pub fn getWindowDecoration(self: *Self) configpkg.WindowDecoration {
const priv = self.private();
if (priv.window_decoration) |v| return v;
if (priv.config) |v| return v.get().@"window-decoration";
return .auto;
}
/// Toggle the window decorations for this window.
pub fn toggleWindowDecorations(self: *Self) void {
const priv = self.private();
if (priv.window_decoration) |_| {
// Unset any previously set window decoration settings
self.setWindowDecoration(null);
return;
}
const config = if (priv.config) |v| v.get() else return;
self.setWindowDecoration(switch (config.@"window-decoration") {
// Use auto when the decoration is initially none
.none => .auto,
// Anything non-none to none
.auto, .client, .server => .none,
});
}
/// Set the window decoration override for this window. If this is null,
/// then we'll revert back to the configuration's default.
fn setWindowDecoration(
self: *Self,
new_: ?configpkg.WindowDecoration,
) void {
const priv = self.private();
priv.window_decoration = new_;
self.syncAppearance();
}
/// Get the currently selected tab as a Tab object.
fn getSelectedTab(self: *Self) ?*Tab {
const priv = self.private();
const page = priv.tab_view.getSelectedPage() orelse return null;
const child = page.getChild();
assert(gobject.ext.isA(child, Tab));
return gobject.ext.cast(Tab, child);
}
/// Returns true if this window needs confirmation before quitting.
fn getNeedsConfirmQuit(self: *Self) bool {
const priv = self.private();
const n = priv.tab_view.getNPages();
assert(n >= 0);
for (0..@intCast(n)) |i| {
const page = priv.tab_view.getNthPage(@intCast(i));
const child = page.getChild();
const tab = gobject.ext.cast(Tab, child) orelse {
log.warn("unexpected non-Tab child in tab view", .{});
continue;
};
if (tab.getNeedsConfirmQuit()) return true;
}
return false;
}
fn isFullscreen(self: *Window) bool {
return self.as(gtk.Window).isFullscreen() != 0;
}
fn isMaximized(self: *Window) bool {
return self.as(gtk.Window).isMaximized() != 0;
}
fn getHeaderbarVisible(self: *Self) bool {
const priv = self.private();
// Never display the header bar when CSDs are disabled.
const csd_enabled = priv.winproto.clientSideDecorationEnabled();
if (!csd_enabled) return false;
// Never display the header bar as a quick terminal.
if (priv.quick_terminal) return false;
// If we're fullscreen we never show the header bar.
if (self.isFullscreen()) return false;
// The remainder needs a config
const config_obj = self.private().config orelse return true;
const config = config_obj.get();
// *Conditionally* disable the header bar when maximized, and
// gtk-titlebar-hide-when-maximized is set
if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") {
return false;
}
return switch (config.@"gtk-titlebar-style") {
// If the titlebar style is tabs never show the titlebar.
.tabs => false,
// If the titlebar style is native show the titlebar if configured
// to do so.
.native => config.@"gtk-titlebar",
};
}
fn getTabsAutohide(self: *Self) bool {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return true;
return switch (config.@"gtk-titlebar-style") {
// If the titlebar style is tabs we cannot autohide.
.tabs => false,
.native => switch (config.@"window-show-tab-bar") {
// Auto we always autohide... obviously.
.auto => true,
// Always we never autohide because we always show the tab bar.
.always => false,
// Never we autohide because it doesn't actually matter,
// since getTabsVisible will return false.
.never => true,
},
};
}
fn getTabsVisible(self: *Self) bool {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return true;
switch (config.@"gtk-titlebar-style") {
.tabs => {
// *Conditionally* disable the tab bar when maximized, the titlebar
// style is tabs, and gtk-titlebar-hide-when-maximized is set.
if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") return false;
// If the titlebar style is tabs the tab bar must always be visible.
return true;
},
.native => {
return switch (config.@"window-show-tab-bar") {
.always, .auto => true,
.never => false,
};
},
}
}
fn getTabsWide(self: *Self) bool {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return true;
return config.@"gtk-wide-tabs";
}
fn getToolbarStyle(self: *Self) adw.ToolbarStyle {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return .raised;
return switch (config.@"gtk-toolbar-style") {
.flat => .flat,
.raised => .raised,
.@"raised-border" => .raised_border,
};
}
fn getTitlebarStyle(self: *Self) TitlebarStyle {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return .native;
return config.@"gtk-titlebar-style";
}
fn propConfig(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
if (priv.config) |config_obj| {
const config = config_obj.get();
if (config.@"app-notifications".@"config-reload") {
self.addToast(i18n._("Reloaded the configuration"));
}
}
self.syncAppearance();
}
fn propGdkSurfaceHeight(
_: *gdk.Surface,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// X11 needs to fix blurring on resize, but winproto implementations
// could do anything.
self.private().winproto.resizeEvent() catch |err| {
log.warn(
"winproto resize event failed error={}",
.{err},
);
};
}
fn propIsActive(
_: *gtk.Window,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// Don't change urgency if we're not the active window.
if (self.as(gtk.Window).isActive() == 0) return;
self.winproto().setUrgent(false) catch |err| {
log.warn(
"winproto failed to reset urgency={}",
.{err},
);
};
}
fn propGdkSurfaceWidth(
_: *gdk.Surface,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// X11 needs to fix blurring on resize, but winproto implementations
// could do anything.
self.private().winproto.resizeEvent() catch |err| {
log.warn(
"winproto resize event failed error={}",
.{err},
);
};
}
fn propFullscreened(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
self.syncAppearance();
}
fn propMaximized(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
self.syncAppearance();
}
fn propMenuActive(
button: *gtk.MenuButton,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// Debian 12 is stuck on GTK 4.8
if (!gtk_version.atLeast(4, 10, 0)) return;
// We only care if we're activating. If we're activating then
// we need to check the validity of our menu items.
const active = button.getActive() != 0;
if (!active) return;
self.syncActions();
}
fn propQuickTerminal(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
if (priv.surface_init) {
log.warn("quick terminal property can't be changed after surfaces have been initialized", .{});
return;
}
if (priv.quick_terminal) {
// Initialize the quick terminal at the app-layer
Application.default().winproto().initQuickTerminal(self) catch |err| {
log.warn("failed to initialize quick terminal error={}", .{err});
return;
};
}
}
fn propScaleFactor(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// On some platforms (namely X11) we need to refresh our appearance when
// the scale factor changes. In theory this could be more fine-grained as
// a full refresh could be expensive, but a) this *should* be rare, and
// b) quite noticeable visual bugs would occur if this is not present.
self.private().winproto.syncAppearance() catch |err| {
log.warn(
"failed to sync appearance after scale factor has been updated={}",
.{err},
);
return;
};
}
fn closureTitlebarStyleIsTab(
_: *Self,
value: TitlebarStyle,
) callconv(.c) c_int {
return @intFromBool(switch (value) {
.native => false,
.tabs => true,
});
}
fn closureSubtitle(
_: *Self,
config_: ?*Config,
pwd_: ?[*:0]const u8,
) callconv(.c) ?[*:0]const u8 {
const config = if (config_) |v| v.get() else return null;
return switch (config.@"window-subtitle") {
.false => null,
.@"working-directory" => pwd: {
const pwd = pwd_ orelse return null;
break :pwd glib.ext.dupeZ(u8, std.mem.span(pwd));
},
};
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
priv.command_palette.set(null);
if (priv.config) |v| {
v.unref();
priv.config = null;
}
priv.tab_bindings.setSource(null);
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
priv.tab_bindings.unref();
priv.winproto.deinit(Application.default().allocator());
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Signal handlers
fn windowRealize(_: *gtk.Widget, self: *Window) callconv(.c) void {
const app = Application.default();
// Initialize our window protocol logic
if (winprotopkg.Window.init(
app.allocator(),
app.winproto(),
self,
)) |wp| {
self.private().winproto = wp;
} else |err| {
log.warn("failed to initialize window protocol error={}", .{err});
return;
}
// We need to setup resize notifications on our surface,
// which is only available after the window had been realized.
if (self.as(gtk.Native).getSurface()) |gdk_surface| {
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceWidth,
self,
.{ .detail = "width" },
);
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceHeight,
self,
.{ .detail = "height" },
);
}
// When we are realized we always setup our appearance since this
// calls some winproto functions.
self.syncAppearance();
}
fn btnNewTab(_: *adw.SplitButton, self: *Self) callconv(.c) void {
self.performBindingAction(.new_tab);
}
fn tabOverviewCreateTab(
_: *adw.TabOverview,
self: *Self,
) callconv(.c) *adw.TabPage {
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null);
}
fn tabOverviewOpen(
tab_overview: *adw.TabOverview,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// We only care about when the tab overview is closed.
if (tab_overview.getOpen() != 0) return;
// On tab overview close, focus is sometimes lost. This is an
// upstream issue in libadwaita[1]. When this is resolved we
// can put a runtime version check here to avoid this workaround.
//
// Our workaround is to start a timer after 500ms to refocus
// the currently selected tab. We choose 500ms because the adw
// animation is 400ms.
//
// [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670
// If we have an old timer remove it
const priv = self.private();
if (priv.tab_overview_focus_timer) |timer| {
_ = glib.Source.remove(timer);
}
// Restart our timer
priv.tab_overview_focus_timer = glib.timeoutAdd(
500,
tabOverviewFocusTimer,
self,
);
}
fn tabOverviewFocusTimer(
ud: ?*anyopaque,
) callconv(.c) c_int {
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
// Always note our timer is removed
self.private().tab_overview_focus_timer = null;
// Get our currently active surface which should respect the newly
// selected tab. Grab focus.
const surface = self.getActiveSurface() orelse return 0;
surface.grabFocus();
// Remove the timer
return 0;
}
fn windowCloseRequest(
_: *gtk.Window,
self: *Self,
) callconv(.c) c_int {
if (self.getNeedsConfirmQuit()) {
// Show a confirmation dialog
const dialog: *CloseConfirmationDialog = .new(.window);
_ = CloseConfirmationDialog.signals.@"close-request".connect(
dialog,
*Self,
closeConfirmationClose,
self,
.{},
);
// Show it
dialog.present(self.as(gtk.Widget));
return @intFromBool(true);
}
self.as(gtk.Window).destroy();
return @intFromBool(false);
}
fn closeConfirmationClose(
_: *CloseConfirmationDialog,
self: *Self,
) callconv(.c) void {
self.as(gtk.Window).destroy();
}
fn closeConfirmationCloseTab(
_: *CloseConfirmationDialog,
page: *adw.TabPage,
) callconv(.c) void {
const tab_view = ext.getAncestor(
adw.TabView,
page.getChild().as(gtk.Widget),
) orelse {
log.warn("close confirmation called for non-existent page", .{});
return;
};
tab_view.closePageFinish(page, @intFromBool(true));
}
fn closeConfirmationCancelTab(
_: *CloseConfirmationDialog,
page: *adw.TabPage,
) callconv(.c) void {
const tab_view = ext.getAncestor(
adw.TabView,
page.getChild().as(gtk.Widget),
) orelse {
log.warn("close confirmation called for non-existent page", .{});
return;
};
tab_view.closePageFinish(page, @intFromBool(false));
}
fn tabViewClosePage(
_: *adw.TabView,
page: *adw.TabPage,
self: *Self,
) callconv(.c) c_int {
const priv = self.private();
const child = page.getChild();
const tab = gobject.ext.cast(Tab, child) orelse
return @intFromBool(false);
// If the tab says it doesn't need confirmation then we go ahead
// and close immediately.
if (!tab.getNeedsConfirmQuit()) {
priv.tab_view.closePageFinish(page, @intFromBool(true));
return @intFromBool(true);
}
// Show a confirmation dialog
const dialog: *CloseConfirmationDialog = .new(.tab);
_ = CloseConfirmationDialog.signals.@"close-request".connect(
dialog,
*adw.TabPage,
closeConfirmationCloseTab,
page,
.{},
);
_ = CloseConfirmationDialog.signals.cancel.connect(
dialog,
*adw.TabPage,
closeConfirmationCancelTab,
page,
.{},
);
// Show it
dialog.present(child);
return @intFromBool(true);
}
fn tabViewSelectedPage(
_: *adw.TabView,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
// Always reset our binding source in case we have no pages.
priv.tab_bindings.setSource(null);
// Get our current page which MUST be a Tab object.
const page = priv.tab_view.getSelectedPage() orelse return;
const child = page.getChild();
assert(gobject.ext.isA(child, Tab));
// Setup our binding group. This ensures things like the title
// are synced from the active tab.
priv.tab_bindings.setSource(child.as(gobject.Object));
// If the tab was previously marked as needing attention
// (e.g. due to a bell character), we now unmark that
page.setNeedsAttention(@intFromBool(false));
}
fn tabViewPageAttached(
_: *adw.TabView,
page: *adw.TabPage,
_: c_int,
self: *Self,
) callconv(.c) void {
// Get the attached page which must be a Tab object.
const child = page.getChild();
const tab = gobject.ext.cast(Tab, child) orelse return;
// Attach listeners for the tab.
_ = Tab.signals.@"close-request".connect(
tab,
*Self,
tabCloseRequest,
self,
.{},
);
// Attach listeners for the surface.
//
// Interesting behavior here that was previously undocumented but
// I'm going to make it explicit here: we accept all the signals here
// (like toggle-fullscreen) regardless of whether the surface or tab
// is focused. At the time of writing this we have no API that could
// really trigger these that way but its theoretically possible.
//
// What is DEFINITELY possible is something like OSC52 triggering
// a clipboard-write signal on an unfocused tab/surface. We definitely
// want to show the user a notification about that but our notification
// right now is a toast that doesn't make it clear WHO used the
// clipboard. We probably want to change that in the future.
//
// I'm not sure how desirable all the above is, and we probably
// should be thoughtful about future signals here. But all of this
// behavior is consistent with macOS and the previous GTK apprt,
// but that behavior was all implicit and not documented, so here
// I am.
if (tab.getSurfaceTree()) |tree| {
self.connectSurfaceHandlers(tree);
}
}
fn tabViewPageDetached(
_: *adw.TabView,
page: *adw.TabPage,
_: c_int,
self: *Self,
) callconv(.c) void {
// We need to get the tab to disconnect the signals.
const child = page.getChild();
const tab = gobject.ext.cast(Tab, child) orelse return;
_ = gobject.signalHandlersDisconnectMatched(
tab.as(gobject.Object),
.{ .data = true },
0,
0,
null,
null,
self,
);
// Remove the tree handlers
if (tab.getSurfaceTree()) |tree| {
self.disconnectSurfaceHandlers(tree);
}
}
fn tabViewCreateWindow(
_: *adw.TabView,
_: *Self,
) callconv(.c) *adw.TabView {
// Create a new window without creating a new tab.
const win = gobject.ext.newInstance(
Self,
.{
.application = Application.default(),
},
);
// We have to show it otherwise it'll just be hidden.
gtk.Window.present(win.as(gtk.Window));
// Get our tab view
return win.private().tab_view;
}
fn tabCloseRequest(
tab: *Tab,
self: *Self,
) callconv(.c) void {
const priv = self.private();
const page = priv.tab_view.getPage(tab.as(gtk.Widget));
// TODO: connect close page handler to tab to check for confirmation
priv.tab_view.closePage(page);
}
fn tabViewNPages(
_: *adw.TabView,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
if (priv.tab_view.getNPages() == 0) {
// If we have no pages left then we want to close window.
// If the tab overview is open, then we don't close the window
// because its a rather abrupt experience. This also fixes an
// issue where dragging out the last tab in the tab overview
// won't cause Ghostty to exit.
if (priv.tab_overview.getOpen() != 0) return;
self.as(gtk.Window).close();
}
}
fn surfaceClipboardWrite(
_: *Surface,
clipboard_type: apprt.Clipboard,
text: [*:0]const u8,
self: *Self,
) callconv(.c) void {
// We only toast for the standard clipboard.
if (clipboard_type != .standard) return;
// We only toast if configured to
const priv = self.private();
const config_obj = priv.config orelse return;
const config = config_obj.get();
if (!config.@"app-notifications".@"clipboard-copy") {
return;
}
if (text[0] != 0)
self.addToast(i18n._("Copied to clipboard"))
else
self.addToast(i18n._("Cleared clipboard"));
}
fn surfaceMenu(
_: *Surface,
self: *Self,
) callconv(.c) void {
self.syncActions();
}
fn surfacePresentRequest(
surface: *Surface,
self: *Self,
) callconv(.c) void {
// Verify that this surface is actually in this window.
{
const surface_window = ext.getAncestor(
Self,
surface.as(gtk.Widget),
) orelse {
log.warn(
"present request called for non-existent surface",
.{},
);
return;
};
if (surface_window != self) {
log.warn(
"present request called for surface in different window",
.{},
);
return;
}
}
// Get the tab for this surface.
const tab = ext.getAncestor(
Tab,
surface.as(gtk.Widget),
) orelse {
log.warn("present request surface not found", .{});
return;
};
// Get the page that contains this tab
const priv = self.private();
const tab_view = priv.tab_view;
const page = tab_view.getPage(tab.as(gtk.Widget));
tab_view.setSelectedPage(page);
// Grab focus
surface.grabFocus();
}
fn surfaceToggleFullscreen(
surface: *Surface,
self: *Self,
) callconv(.c) void {
_ = surface;
if (self.as(gtk.Window).isFullscreen() != 0) {
self.as(gtk.Window).unfullscreen();
} else {
self.as(gtk.Window).fullscreen();
}
// We react to the changes in the propFullscreen callback
}
fn surfaceToggleMaximize(
surface: *Surface,
self: *Self,
) callconv(.c) void {
_ = surface;
if (self.as(gtk.Window).isMaximized() != 0) {
self.as(gtk.Window).unmaximize();
} else {
self.as(gtk.Window).maximize();
}
// We react to the changes in the propMaximized callback
}
fn surfaceInit(
surface: *Surface,
self: *Self,
) callconv(.c) void {
const priv = self.private();
// Make sure we init only once
if (priv.surface_init) return;
priv.surface_init = true;
// Setup our default and minimum size.
if (surface.getDefaultSize()) |size| {
self.as(gtk.Window).setDefaultSize(
@intCast(size.width),
@intCast(size.height),
);
}
if (surface.getMinSize()) |size| {
self.as(gtk.Widget).setSizeRequest(
@intCast(size.width),
@intCast(size.height),
);
}
}
fn tabSplitTreeChanged(
_: *SplitTree,
old_tree: ?*const Surface.Tree,
new_tree: ?*const Surface.Tree,
self: *Self,
) callconv(.c) void {
if (old_tree) |tree| {
self.disconnectSurfaceHandlers(tree);
}
if (new_tree) |tree| {
self.connectSurfaceHandlers(tree);
}
}
fn actionAbout(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const name = "Ghostty";
const icon = "com.mitchellh.ghostty";
const website = "https://ghostty.org";
if (adw_version.supportsDialogs()) {
adw.showAboutDialog(
self.as(gtk.Widget),
"application-name",
name,
"developer-name",
i18n._("Ghostty Developers"),
"application-icon",
icon,
"version",
build_config.version_string.ptr,
"issue-url",
"https://github.com/ghostty-org/ghostty/issues",
"website",
website,
@as(?*anyopaque, null),
);
} else {
gtk.showAboutDialog(
self.as(gtk.Window),
"program-name",
name,
"logo-icon-name",
icon,
"title",
i18n._("About Ghostty"),
"version",
build_config.version_string.ptr,
"website",
website,
@as(?*anyopaque, null),
);
}
}
fn actionClose(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
self.as(gtk.Window).close();
}
fn actionCloseTab(
_: *gio.SimpleAction,
param_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
const param = param_ orelse {
log.warn("win.close-tab called without a parameter", .{});
return;
};
var str: ?[*:0]const u8 = null;
param.get("&s", &str);
const mode = std.meta.stringToEnum(
input.Binding.Action.CloseTabMode,
std.mem.span(
str orelse {
log.warn("invalid mode provided to win.close-tab", .{});
return;
},
),
) orelse {
log.warn("invalid mode provided to win.close-tab: {s}", .{str.?});
return;
};
self.performBindingAction(.{ .close_tab = mode });
}
fn actionNewWindow(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.new_window);
}
fn actionNewTab(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.new_tab);
}
fn actionSplitRight(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .right });
}
fn actionSplitLeft(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .left });
}
fn actionSplitUp(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .up });
}
fn actionSplitDown(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .down });
}
fn actionCopy(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.copy_to_clipboard);
}
fn actionPaste(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.paste_from_clipboard);
}
fn actionReset(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.reset);
}
fn actionClear(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.clear_screen);
}
fn actionRingBell(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return;
if (config.@"bell-features".system) system: {
const native = self.as(gtk.Native).getSurface() orelse {
log.warn("unable to get native surface from window", .{});
break :system;
};
native.beep();
}
if (config.@"bell-features".attention) attention: {
// Dont set urgency if the window is already active.
if (self.as(gtk.Window).isActive() != 0) break :attention;
// Request user attention
self.winproto().setUrgent(true) catch |err| {
log.warn("winproto failed to set urgency={}", .{err});
};
}
}
/// Toggle the command palette.
///
/// TODO: accept the surface that toggled the command palette as a parameter
fn toggleCommandPalette(self: *Window) void {
const priv = self.private();
// Get a reference to a command palette. First check the weak reference
// that we save to see if we already have one stored. If we don't then
// create a new one.
const command_palette = priv.command_palette.get() orelse command_palette: {
// Create a fresh command palette.
const command_palette = CommandPalette.new();
// Synchronize our config to the command palette's config.
_ = gobject.Object.bindProperty(
self.as(gobject.Object),
"config",
command_palette.as(gobject.Object),
"config",
.{ .sync_create = true },
);
// Listen to the activate signal to know if the user selected an option in
// the command palette.
_ = CommandPalette.signals.trigger.connect(
command_palette,
*Window,
signalCommandPaletteTrigger,
self,
.{},
);
// Save a weak reference to the command palette. We use a weak reference to avoid
// reference counting cycles that might cause problems later.
priv.command_palette.set(command_palette);
break :command_palette command_palette;
};
defer command_palette.unref();
// Tell the command palette to toggle itself. If the dialog gets
// presented (instead of hidden) it will be modal over our window.
command_palette.toggle(self);
}
// React to a signal from a command palette asking an action to be performed.
fn signalCommandPaletteTrigger(_: *CommandPalette, action: *const input.Binding.Action, self: *Self) callconv(.c) void {
// If the activation actually has an action, perform it.
self.performBindingAction(action.*);
}
/// React to a GTK action requesting that the command palette be toggled.
fn actionToggleCommandPalette(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
// TODO: accept the surface that toggled the command palette as a
// parameter
self.toggleCommandPalette();
}
/// Toggle the Ghostty inspector for the active surface.
fn toggleInspector(self: *Self) void {
const surface = self.getActiveSurface() orelse return;
_ = surface.controlInspector(.toggle);
}
/// React to a GTK action requesting that the Ghostty inspector be toggled.
fn actionToggleInspector(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
// TODO: accept the surface that toggled the command palette as a
// parameter
self.toggleInspector();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(DebugWarning);
gobject.ext.ensureType(SplitTree);
gobject.ext.ensureType(Surface);
gobject.ext.ensureType(Tab);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "window",
}),
);
// Properties
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.config.impl,
properties.debug.impl,
properties.@"headerbar-visible".impl,
properties.@"quick-terminal".impl,
properties.@"tabs-autohide".impl,
properties.@"tabs-visible".impl,
properties.@"tabs-wide".impl,
properties.@"toolbar-style".impl,
properties.@"titlebar-style".impl,
});
// Bindings
class.bindTemplateChildPrivate("tab_overview", .{});
class.bindTemplateChildPrivate("tab_bar", .{});
class.bindTemplateChildPrivate("tab_view", .{});
class.bindTemplateChildPrivate("toolbar", .{});
class.bindTemplateChildPrivate("toast_overlay", .{});
// Template Callbacks
class.bindTemplateCallback("realize", &windowRealize);
class.bindTemplateCallback("new_tab", &btnNewTab);
class.bindTemplateCallback("overview_create_tab", &tabOverviewCreateTab);
class.bindTemplateCallback("overview_notify_open", &tabOverviewOpen);
class.bindTemplateCallback("close_request", &windowCloseRequest);
class.bindTemplateCallback("close_page", &tabViewClosePage);
class.bindTemplateCallback("page_attached", &tabViewPageAttached);
class.bindTemplateCallback("page_detached", &tabViewPageDetached);
class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow);
class.bindTemplateCallback("notify_n_pages", &tabViewNPages);
class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
class.bindTemplateCallback("notify_is_active", &propIsActive);
class.bindTemplateCallback("notify_maximized", &propMaximized);
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
class.bindTemplateCallback("notify_scale_factor", &propScaleFactor);
class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab);
class.bindTemplateCallback("computed_subtitle", &closureSubtitle);
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};