apprt/gtk: refactor into separate files

This commit is contained in:
Mitchell Hashimoto
2023-09-16 08:21:51 -07:00
parent cbbafc70e2
commit d5036e78f4
5 changed files with 1839 additions and 1797 deletions

File diff suppressed because it is too large Load Diff

300
src/apprt/gtk/App.zig Normal file
View File

@ -0,0 +1,300 @@
/// App is the entrypoint for the application. This is called after all
/// of the runtime-agnostic initialization is complete and we're ready
/// to start.
///
/// There is only ever one App instance per process. This is because most
/// application frameworks also have this restriction so it simplifies
/// the assumptions.
///
/// In GTK, the App contains the primary GApplication and GMainContext
/// (event loop) along with any global app state.
const App = @This();
const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
const c = @import("c.zig");
const log = std.log.scoped(.gtk);
pub const Options = struct {};
core_app: *CoreApp,
config: Config,
app: *c.GtkApplication,
ctx: *c.GMainContext,
/// The "none" cursor. We use one that is shared across the entire app.
cursor_none: ?*c.GdkCursor,
/// This is set to false when the main loop should exit.
running: bool = true,
pub fn init(core_app: *CoreApp, opts: Options) !App {
_ = opts;
// This is super weird, but we still use GLFW with GTK only so that
// we can tap into their folklore logic to get screen DPI. If we can
// figure out a reliable way to determine this ourselves, we can get
// rid of this dep.
if (!glfw.init(.{})) return error.GlfwInitFailed;
// Load our configuration
var config = try Config.load(core_app.alloc);
errdefer config.deinit();
// If we had configuration errors, then log them.
if (!config._errors.empty()) {
for (config._errors.list.items) |err| {
log.warn("configuration error: {s}", .{err.message});
}
}
// The "none" cursor is used for hiding the cursor
const cursor_none = c.gdk_cursor_new_from_name("none", null);
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
// Our uniqueness ID is based on whether we're in a debug mode or not.
// In debug mode we want to be separate so we can develop Ghostty in
// Ghostty.
const uniqueness_id: ?[*c]const u8 = uniqueness_id: {
if (!config.@"gtk-single-instance") break :uniqueness_id null;
break :uniqueness_id "com.mitchellh.ghostty" ++ if (builtin.mode == .Debug) "-debug" else "";
};
// Create our GTK Application which encapsulates our process.
const app = @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
uniqueness_id orelse null,
c.G_APPLICATION_DEFAULT_FLAGS,
))) orelse return error.GtkInitFailed;
errdefer c.g_object_unref(app);
_ = c.g_signal_connect_data(
app,
"activate",
c.G_CALLBACK(&activate),
core_app,
null,
c.G_CONNECT_DEFAULT,
);
// We don't use g_application_run, we want to manually control the
// loop so we have to do the same things the run function does:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
const ctx = c.g_main_context_default() orelse return error.GtkContextFailed;
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
errdefer c.g_main_context_release(ctx);
const gapp = @as(*c.GApplication, @ptrCast(app));
var err_: ?*c.GError = null;
if (c.g_application_register(
gapp,
null,
@ptrCast(&err_),
) == 0) {
if (err_) |err| {
log.warn("error registering application: {s}", .{err.message});
c.g_error_free(err);
}
return error.GtkApplicationRegisterFailed;
}
// This just calls the "activate" signal but its part of the normal
// startup routine so we just call it:
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
c.g_application_activate(gapp);
return .{
.core_app = core_app,
.app = app,
.config = config,
.ctx = ctx,
.cursor_none = cursor_none,
// If we are NOT the primary instance, then we never want to run.
// This means that another instance of the GTK app is running and
// our "activate" call above will open a window.
.running = c.g_application_get_is_remote(gapp) == 0,
};
}
// Terminate the application. The application will not be restarted after
// this so all global state can be cleaned up.
pub fn terminate(self: *App) void {
c.g_settings_sync();
while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
c.g_main_context_release(self.ctx);
c.g_object_unref(self.app);
if (self.cursor_none) |cursor| c.g_object_unref(cursor);
self.config.deinit();
glfw.terminate();
}
/// Reload the configuration. This should return the new configuration.
/// The old value can be freed immediately at this point assuming a
/// successful return.
///
/// The returned pointer value is only valid for a stable self pointer.
pub fn reloadConfig(self: *App) !?*const Config {
// Load our configuration
var config = try Config.load(self.core_app.alloc);
errdefer config.deinit();
// Update the existing config, be sure to clean up the old one.
self.config.deinit();
self.config = config;
return &self.config;
}
/// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: App) void {
_ = self;
c.g_main_context_wakeup(null);
}
/// Run the event loop. This doesn't return until the app exits.
pub fn run(self: *App) !void {
while (self.running) {
_ = c.g_main_context_iteration(self.ctx, 1);
// Tick the terminal app
const should_quit = try self.core_app.tick(self);
if (should_quit) self.quit();
}
}
/// Close the given surface.
pub fn redrawSurface(self: *App, surface: *Surface) void {
_ = self;
surface.redraw();
}
/// Called by CoreApp to create a new window with a new surface.
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
const alloc = self.core_app.alloc;
// Allocate a fixed pointer for our window. 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.
var window = try alloc.create(Window);
errdefer alloc.destroy(window);
try window.init(self);
// Add our initial tab
try window.newTab(parent_);
}
fn quit(self: *App) void {
// If we have no toplevel windows, then we're done.
const list = c.gtk_window_list_toplevels();
if (list == null) {
self.running = false;
return;
}
c.g_list_free(list);
// If the app says we don't need to confirm, then we can quit now.
if (!self.core_app.needsConfirmQuit()) {
self.quitNow();
return;
}
// If we have windows, then we want to confirm that we want to exit.
const alert = c.gtk_message_dialog_new(
null,
c.GTK_DIALOG_MODAL,
c.GTK_MESSAGE_QUESTION,
c.GTK_BUTTONS_YES_NO,
"Quit Ghostty?",
);
c.gtk_message_dialog_format_secondary_text(
@ptrCast(alert),
"All active terminal sessions will be terminated.",
);
// We want the "yes" to appear destructive.
const yes_widget = c.gtk_dialog_get_widget_for_response(
@ptrCast(alert),
c.GTK_RESPONSE_YES,
);
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
// We want the "no" to be the default action
c.gtk_dialog_set_default_response(
@ptrCast(alert),
c.GTK_RESPONSE_NO,
);
_ = c.g_signal_connect_data(
alert,
"response",
c.G_CALLBACK(&gtkQuitConfirmation),
self,
null,
c.G_CONNECT_DEFAULT,
);
c.gtk_widget_show(alert);
}
/// This immediately destroys all windows, forcing the application to quit.
fn quitNow(self: *App) void {
_ = self;
const list = c.gtk_window_list_toplevels();
defer c.g_list_free(list);
c.g_list_foreach(list, struct {
fn callback(data: c.gpointer, _: c.gpointer) callconv(.C) void {
const ptr = data orelse return;
const widget: *c.GtkWidget = @ptrCast(@alignCast(ptr));
const window: *c.GtkWindow = @ptrCast(widget);
c.gtk_window_destroy(window);
}
}.callback, null);
}
fn gtkQuitConfirmation(
alert: *c.GtkMessageDialog,
response: c.gint,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
// Close the alert window
c.gtk_window_destroy(@ptrCast(alert));
// If we didn't confirm then we're done
if (response != c.GTK_RESPONSE_YES) return;
// Force close all open windows
self.quitNow();
}
/// This is called by the "activate" signal. This is sent on program
/// startup and also when a secondary instance launches and requests
/// a new window.
fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
_ = app;
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
// Queue a new window
_ = core_app.mailbox.push(.{
.new_window = .{},
}, .{ .forever = {} });
}

1120
src/apprt/gtk/Surface.zig Normal file

File diff suppressed because it is too large Load Diff

404
src/apprt/gtk/Window.zig Normal file
View File

@ -0,0 +1,404 @@
/// A Window is a single, real GTK window.
const Window = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const configpkg = @import("../../config.zig");
const font = @import("../../font/main.zig");
const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const Surface = @import("Surface.zig");
const c = @import("c.zig");
const log = std.log.scoped(.gtk);
const TAB_CLOSE_PAGE = "tab_close_page";
const TAB_CLOSE_SURFACE = "tab_close_surface";
app: *App,
/// Our window
window: *c.GtkWindow,
/// The notebook (tab grouping) for this window.
notebook: *c.GtkNotebook,
/// The background CSS for the window (if any).
css_window_background: ?[]u8 = null,
/// The resources directory for the icon (if any).
icon_search_dir: ?[:0]const u8 = null,
pub fn init(self: *Window, app: *App) !void {
// Set up our own state
self.* = .{
.app = app,
.window = undefined,
.notebook = undefined,
};
// Create the window
const window = c.gtk_application_window_new(app.app);
const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window);
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty");
c.gtk_window_set_default_size(gtk_window, 1000, 600);
// If we don't have the icon then we'll try to add our resources dir
// to the search path and see if we can find it there.
const icon_name = "com.mitchellh.ghostty";
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(window));
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
const base = self.app.core_app.resources_dir orelse {
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
log.info("gtk app will not have Ghostty icon", .{});
break :icon;
};
// Note that this method for adding the icon search path is
// a fallback mechanism. The recommended mechanism is the
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
// file in zig-out/share that should be installed to the proper
// place.
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
self.icon_search_dir = dir;
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
log.warn("Ghostty icon for gtk app not found", .{});
}
}
c.gtk_window_set_icon_name(gtk_window, icon_name);
// Apply background opacity if we have it
if (app.config.@"background-opacity" < 1) {
c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity");
}
// Hide window decoration if configured. This has to happen before
// `gtk_widget_show`.
if (!app.config.@"window-decoration") {
c.gtk_window_set_decorated(gtk_window, 0);
}
c.gtk_widget_show(window);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
// Create a notebook to hold our tabs.
const notebook_widget = c.gtk_notebook_new();
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
self.notebook = notebook;
c.gtk_notebook_set_tab_pos(notebook, c.GTK_POS_TOP);
c.gtk_notebook_set_scrollable(notebook, 1);
c.gtk_notebook_set_show_tabs(notebook, 0);
c.gtk_notebook_set_show_border(notebook, 0);
// This is important so the notebook expands to fit available space.
// Otherwise, it will be zero/zero in the box below.
c.gtk_widget_set_vexpand(notebook_widget, 1);
c.gtk_widget_set_hexpand(notebook_widget, 1);
// Create our add button for new tabs
const notebook_add_btn = c.gtk_button_new_from_icon_name("list-add-symbolic");
c.gtk_notebook_set_action_widget(notebook, notebook_add_btn, c.GTK_PACK_END);
_ = c.g_signal_connect_data(notebook_add_btn, "clicked", c.G_CALLBACK(&gtkTabAddClick), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), self, null, c.G_CONNECT_DEFAULT);
// Create our box which will hold our widgets.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
// In debug we show a warning. This is a really common issue where
// people build from source in debug and performance is really bad.
if (builtin.mode == .Debug) {
const warning = c.gtk_label_new("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
c.gtk_widget_set_margin_top(warning, 10);
c.gtk_widget_set_margin_bottom(warning, 10);
c.gtk_box_append(@ptrCast(box), warning);
}
c.gtk_box_append(@ptrCast(box), notebook_widget);
// The box is our main child
c.gtk_window_set_child(gtk_window, box);
}
pub fn deinit(self: *Window) void {
if (self.css_window_background) |ptr| self.app.core_app.alloc.free(ptr);
if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr);
}
/// Add a new tab to this window.
pub fn newTab(self: *Window, parent_: ?*CoreSurface) !void {
// Grab a surface allocation we'll need it later.
var surface = try self.app.core_app.alloc.create(Surface);
errdefer self.app.core_app.alloc.destroy(surface);
// Inherit the parent's font size if we are configured to.
const font_size: ?font.face.DesiredSize = font_size: {
if (!self.app.config.@"window-inherit-font-size") break :font_size null;
const parent = parent_ orelse break :font_size null;
break :font_size parent.font_size;
};
// Build our 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 = c.gtk_label_new("Ghostty");
c.gtk_box_append(label_box, label_text);
const label_close_widget = c.gtk_button_new_from_icon_name("window-close");
const label_close = @as(*c.GtkButton, @ptrCast(label_close_widget));
c.gtk_button_set_has_frame(label_close, 0);
c.gtk_box_append(label_box, label_close_widget);
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&gtkTabCloseClick), self, null, c.G_CONNECT_DEFAULT);
// Initialize the GtkGLArea and attach it to our surface.
// The surface starts in the "unrealized" state because we have to
// wait for the "realize" callback from GTK to know that the OpenGL
// context is ready. See Surface docs for more info.
const gl_area = c.gtk_gl_area_new();
try surface.init(self.app, .{
.window = self,
.gl_area = @ptrCast(gl_area),
.title_label = @ptrCast(label_text),
.font_size = font_size,
});
errdefer surface.deinit();
const page_idx = c.gtk_notebook_append_page(self.notebook, gl_area, label_box_widget);
if (page_idx < 0) {
log.warn("failed to add surface to notebook", .{});
return error.GtkAppendPageFailed;
}
// Tab settings
c.gtk_notebook_set_tab_reorderable(self.notebook, gl_area, 1);
// If we have multiple tabs, show the tab bar.
if (c.gtk_notebook_get_n_pages(self.notebook) > 1) {
c.gtk_notebook_set_show_tabs(self.notebook, 1);
}
// Set the userdata of the close button so it points to this page.
const page = c.gtk_notebook_get_page(self.notebook, gl_area) orelse
return error.GtkNotebookPageNotFound;
c.g_object_set_data(@ptrCast(label_close), TAB_CLOSE_SURFACE, surface);
c.g_object_set_data(@ptrCast(label_close), TAB_CLOSE_PAGE, page);
c.g_object_set_data(@ptrCast(gl_area), TAB_CLOSE_PAGE, page);
// Switch to the new tab
c.gtk_notebook_set_current_page(self.notebook, page_idx);
// We need to grab focus after it is added to the window. When
// creating a window we want to always focus on the widget.
const widget = @as(*c.GtkWidget, @ptrCast(gl_area));
_ = c.gtk_widget_grab_focus(widget);
}
/// Close the tab for the given notebook page. This will automatically
/// handle closing the window if there are no more tabs.
fn closeTab(self: *Window, page: *c.GtkNotebookPage) void {
// Remove the page
const page_idx = getNotebookPageIndex(page);
c.gtk_notebook_remove_page(self.notebook, page_idx);
const remaining = c.gtk_notebook_get_n_pages(self.notebook);
switch (remaining) {
// If we have no more tabs we close the window
0 => c.gtk_window_destroy(self.window),
// If we have one more tab we hide the tab bar
1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
else => {},
}
// If we have remaining tabs, we need to make sure we grab focus.
if (remaining > 0) self.focusCurrentTab();
}
/// Close the surface. This surface must be definitely part of this window.
pub fn closeSurface(self: *Window, surface: *Surface) void {
assert(surface.window == self);
self.closeTab(getNotebookPage(@ptrCast(surface.gl_area)) orelse return);
}
/// Go to the previous tab for a surface.
pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
const page = getNotebookPage(@ptrCast(surface.gl_area)) orelse return;
const page_idx = getNotebookPageIndex(page);
// The next index is the previous or we wrap around.
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
const max = c.gtk_notebook_get_n_pages(self.notebook);
break :next_idx max -| 1;
};
// Do nothing if we have one tab
if (next_idx == page_idx) return;
c.gtk_notebook_set_current_page(self.notebook, next_idx);
self.focusCurrentTab();
}
/// Go to the next tab for a surface.
pub fn gotoNextTab(self: *Window, surface: *Surface) void {
const page = getNotebookPage(@ptrCast(surface.gl_area)) orelse return;
const page_idx = getNotebookPageIndex(page);
const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1;
const next_idx = if (page_idx < max) page_idx + 1 else 0;
if (next_idx == page_idx) return;
c.gtk_notebook_set_current_page(self.notebook, next_idx);
self.focusCurrentTab();
}
/// Go to the specific tab index.
pub fn gotoTab(self: *Window, n: usize) void {
if (n == 0) return;
const max = c.gtk_notebook_get_n_pages(self.notebook);
const page_idx = std.math.cast(c_int, n - 1) orelse return;
if (page_idx < max) {
c.gtk_notebook_set_current_page(self.notebook, page_idx);
self.focusCurrentTab();
}
}
/// Toggle fullscreen for this window.
pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void {
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
if (is_fullscreen == 0) {
c.gtk_window_fullscreen(self.window);
} else {
c.gtk_window_unfullscreen(self.window);
}
}
/// Grabs focus on the currently selected tab.
fn focusCurrentTab(self: *Window) void {
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
const widget = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
_ = c.gtk_widget_grab_focus(widget);
}
fn gtkTabAddClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
const self = userdataSelf(ud.?);
const parent = self.app.core_app.focusedSurface();
self.newTab(parent) catch |err| {
log.warn("error adding new tab: {}", .{err});
return;
};
}
fn gtkTabCloseClick(btn: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
_ = ud;
const surface: *Surface = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(btn), TAB_CLOSE_SURFACE) orelse return,
));
surface.core_surface.close();
}
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
const self = userdataSelf(ud.?);
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
const label_text = c.gtk_label_get_text(gtk_label);
c.gtk_window_set_title(self.window, label_text);
}
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
_ = v;
log.debug("window close request", .{});
const self = userdataSelf(ud.?);
// If none of our surfaces need confirmation, we can just exit.
for (self.app.core_app.surfaces.items) |surface| {
if (surface.window == self) {
if (surface.core_surface.needsConfirmQuit()) break;
}
} else {
c.gtk_window_destroy(self.window);
return true;
}
// Setup our basic message
const alert = c.gtk_message_dialog_new(
self.window,
c.GTK_DIALOG_MODAL,
c.GTK_MESSAGE_QUESTION,
c.GTK_BUTTONS_YES_NO,
"Close this window?",
);
c.gtk_message_dialog_format_secondary_text(
@ptrCast(alert),
"All terminal sessions in this window will be terminated.",
);
// We want the "yes" to appear destructive.
const yes_widget = c.gtk_dialog_get_widget_for_response(
@ptrCast(alert),
c.GTK_RESPONSE_YES,
);
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
// We want the "no" to be the default action
c.gtk_dialog_set_default_response(
@ptrCast(alert),
c.GTK_RESPONSE_NO,
);
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(&gtkCloseConfirmation), self, null, c.G_CONNECT_DEFAULT);
c.gtk_widget_show(alert);
return true;
}
fn gtkCloseConfirmation(
alert: *c.GtkMessageDialog,
response: c.gint,
ud: ?*anyopaque,
) callconv(.C) void {
c.gtk_window_destroy(@ptrCast(alert));
if (response == c.GTK_RESPONSE_YES) {
const self = userdataSelf(ud.?);
c.gtk_window_destroy(self.window);
}
}
/// "destroy" signal for the window
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
_ = v;
log.debug("window destroy", .{});
const self = userdataSelf(ud.?);
const alloc = self.app.core_app.alloc;
self.deinit();
alloc.destroy(self);
}
/// Get the GtkNotebookPage for the given object. You must be sure the
/// object has the notebook page property set.
fn getNotebookPage(obj: *c.GObject) ?*c.GtkNotebookPage {
return @ptrCast(@alignCast(
c.g_object_get_data(obj, TAB_CLOSE_PAGE) orelse return null,
));
}
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 userdataSelf(ud: *anyopaque) *Window {
return @ptrCast(@alignCast(ud));
}

11
src/apprt/gtk/c.zig Normal file
View File

@ -0,0 +1,11 @@
const c = @cImport({
@cInclude("gtk/gtk.h");
});
pub usingnamespace c;
/// Compatibility with gobject < 2.74
pub usingnamespace if (@hasDecl(c, "G_CONNECT_DEFAULT")) struct {} else struct {
pub const G_CONNECT_DEFAULT = 0;
pub const G_APPLICATION_DEFAULT_FLAGS = c.G_APPLICATION_FLAGS_NONE;
};