C+
Packages · View as Markdown

gtk

Typed bindings for GTK 4, the cross-platform (Linux/BSD-first, also Windows/macOS) GUI toolkit. It is the Linux counterpart to appkit (macOS), organized the same way. Sub-modules cover the framework: glib, application, window, widget, controls, containers, text, dialogs, graphics, cairo, menu, actions, and data, plus the import-directly convert, events, pasteboard, drag, and notifications.

Unlike Cocoa, GTK is a plain C / GObject library, so there is no objc_msgSend dispatch: every binding is a direct extern fn to a gtk_* / g_* symbol. That makes the package a thin layer of C+ structs over GObject pointers. Import the umbrella facade for the flat type surface, or pull narrower modules directly:

import "gtk/gtk" as gtk;        // flat surface: gtk::Window, gtk::Button, …
import "gtk/glib" as glib;      // or narrower modules
import "gtk/controls" as controls;

Signals are closure-free

GTK callbacks are plain C function pointers, so handlers are ordinary C+ functions, in keeping with C+ having no closures. The dominant shape is fn(emitter: *u8, user_data: *u8) (a button "clicked", a switch "toggled", a range "value-changed"); a few signals take a different shape, handled by glib::signal_connect3 for (emitter, arg1, user_data) and glib::signal_connect_state for the switch "state-set". Thread app state to a handler through the signal's user_data pointer, an associated object (glib::set_data / get_data), or a global.

import "gtk/gtk" as gtk;
import "gtk/glib" as glib;

fn on_clicked(button: *u8, _data: *u8) {
    let b = gtk::Button::from_raw(button);
    b.set_label(#str_ptr("Clicked!\0"));
}

fn on_activate(app: *u8, _data: *u8) {
    let win = gtk::Window::for_application(app);
    win.set_title(#str_ptr("C+ GTK\0"));
    win.set_default_size(360 as i32, 240 as i32);

    let vbox = gtk::Box::new(glib::orientation_vertical(), 12 as i32);
    gtk::Widget::from_raw(vbox.raw()).set_margin(16 as i32);

    let label = gtk::Label::new(#str_ptr("Hello from C+ via GTK 4\0"));
    vbox.append(label.raw());

    let btn = gtk::Button::new(#str_ptr("Press me\0"));
    btn.connect_clicked(on_clicked);
    vbox.append(btn.raw());

    win.set_child(vbox.raw());
    win.present();
}

fn main() -> i32 {
    let app = gtk::Application::new(#str_ptr("org.example.Hello\0"));
    app.connect_activate(on_activate);
    let status = app.run();
    app.unref();
    return status;
}

Application::new takes a reverse-DNS application id and holds one owned reference. Build your first window in the "activate" handler, where Window::for_application(app) gives you an application window already owned by its GtkApplication. A GTK 4 window holds a single child, so host many widgets in a containers::Box or Grid and present() the window to show it.

Ownership: floating references

GObject widgets use floating references: a freshly-created widget owns one floating ref, and the container it is added to (Box::append, Window::set_child, …) sinks that ref and takes over the lifetime. So the common case needs no manual reference counting — add a widget to its parent and forget it.

Accordingly every widget wrapper holds an opaque obj: *u8 with no drop (the same conservative default as appkit's child widgets) and exposes raw() (the pointer, for packing into containers) plus from_raw(*u8) (to wrap a pointer a signal handed you). The objects you do own a full reference to — Application, a Menu/Action model, an AlertDialog/FileDialog — expose an explicit unref(); call it once you are done (for Application, after run() returns). Hold a widget wrapper in a local across the append call rather than passing Widget::new().raw() inline and letting the temporary drop.

Controls, containers, and dialogs

controls covers Button, Label, Entry, CheckButton, Switch, Scale (slider), SpinButton, ProgressBar, ToggleButton, and DropDown. containers covers Box, Grid, Stack (+ StackSwitcher), Notebook, Paned, ScrolledWindow, Frame, CenterBox, ListBox, and HeaderBar. The modern GTK 4.10+ AlertDialog and FileDialog live in dialogs; menus are data that reference actions by name, split across menu (Menu, MenuButton, PopoverMenu) and actions (Action). Model-backed ListView / ColumnView and their factories live in data.

import "gtk/dialogs" as dialogs;

let dlg = dialogs::AlertDialog::new(#str_ptr("Delete this file?\0"));
dlg.show(win.raw());
dlg.unref();

Input event controllers — ClickGesture, KeyController, MotionController — live in the import-directly events module and attach with events::add_controller(widget, controller).

Platform notes

GTK is a real native dependency, so a GTK app links and runs on the host with cpc build. Install GTK 4 first; on Debian/Ubuntu that is sudo apt install libgtk-4-dev, which provides the gtk4 pkg-config file and the unversioned .so link symlinks the linker needs. The package's [link] libs (gtk-4, gobject-2.0, gio-2.0, glib-2.0, cairo) land on the consumer's link line; the loader resolves the rest of the stack (pango, gdk-pixbuf, graphene, harfbuzz) transitively. The convert and data modules depend on stdlib; the widget modules themselves are stdlib-free.

For the higher-level, declarative UI surface, see facet and builder blocks. For where GTK fits among build outputs, see targets; to drive an app from an external agent, see the agent surface.