Basics

Guides

API Reference

Menu

Basics

Guides

API Reference

GTK4 Usage

What GTK4 is

GTK4 is an open-source toolkit for building desktop graphical user interfaces on Linux, macOS, and Windows. It ships as a set of C libraries (libgtk-4.so.1, libgdk-4, libgobject-2.0, ...) that draw windows, buttons, text inputs, menus, and every other common UI control, along with the plumbing needed to route keyboard, mouse, and touch input back to your code.

The Aussom bindings give you a full Aussom-native wrapper around GTK4. Under the hood the bindings call the exact same C functions that a C, Python, or Rust GTK4 app would call. The Aussom wrappers handle the GObject reference counting, type casts, string marshaling, and callback glue so your code stays in Aussom.

You do not need to know anything about GObject, C signals, or the GTK C API to read this guide. You do need a baseline understanding of Aussom classes, callbacks (::methodName), and include statements.

How the bindings are organized

Every GTK type has its own file under src/com/lehman/aussom/stdlib/aus/gtk/<namespace>/<type>.aus. A GtkButton lives at gtk/gtk/button.aus, a GtkWindow at gtk/gtk/window.aus, a GdkSurface at gtk/gdk/surface.aus, and so on. Each file defines one Aussom class whose name matches the GTK type with the Gtk, Gdk, or G prefix stripped.

You pull a type into your script with an include:

include gtk.gtk.window;
include gtk.gtk.button;
include gtk.gtk.label;

After those includes, new Window(), new Button("Submit"), and new Label("Hi") are all legal Aussom expressions and return live GTK objects.

For namespace-wide symbols -- free functions, enum conversion helpers, and constants -- the bindings provide:

include gtk.gtk.enums;        // enum values like Orientation.vertical
include gtk.gtk.bitfields;    // bitfield helpers
include gtk.gtk.constants;    // GTK integer and string constants
include gtk.gtk.functions;    // free functions, GtkApi helper class

A quick way to explore what a type offers is to read its generated file directly. The docstrings on each method come from the GIR metadata, which means the same docs you would read in the official GTK documentation are in the Aussom wrapper.

Key building blocks

Before any code, here are the concepts that recur in every GTK app.

The widget

Almost everything you see on the screen is a GtkWidget. A button is a widget. A label is a widget. An entry is a widget. A layout box that holds other widgets is also a widget. Every control in GTK4 inherits from GtkWidget, so every control gets the common widget operations: show, hide, set a name, grab focus, register keyboard shortcuts, apply a CSS class, and so on.

In Aussom, every widget wrapper has a handy asWidget() method that returns the same object typed as a Widget, so you can call common widget methods regardless of the specific subclass:

btn = new Button("Save");
btn.asWidget().set_name("saveBtn");       // id for selectors
btn.asWidget().add_css_class("primary");
btn.asWidget().set_sensitive(false);      // grey it out

Widgets form a tree

Widgets are arranged in a tree. A top-level Window holds one child widget, which is usually a layout container, which holds its own children, which can be other containers, all the way down to the leaves (buttons, labels, entries). GTK draws the tree top-down and delivers input events up the tree until something handles them.

In code, building a UI is building the tree:

// A window with a vertical box of two widgets.
win = new Window();
box = new Box(Orientation.vertical, 8);
box.append(new Label("Hello"));
box.append(new Button("OK"));
win.set_child(box);

Application, window, widget

These are three different kinds of GTK object that beginners often confuse. The short version:

  • GtkApplication is the process-level object. One per app. It owns the main event loop, tracks open windows, and handles the "activate" callback that fires on startup. Inherits from GApplication (from Gio).
  • GtkWindow is a top-level OS window. Zero or more per app. Each window has exactly one child widget -- usually a layout container.
  • GtkWidget is the base class of everything inside a window. Buttons, entries, labels, boxes, grids, all widgets.

You do not strictly need a GtkApplication: you can build and present a standalone GtkWindow if you drive the event loop yourself. Using GtkApplication is the more conventional path because it handles desktop integration (dock icon, unique-instance enforcement, .desktop file association) for free.

The event loop

GTK is event-driven. Your code builds the widget tree, wires up callbacks, presents a window, and then returns control to GTK's main loop. The loop waits for keyboard, mouse, and timer events, dispatches them to the widgets that care, and repaints when the scene changes.

Two common ways to run the loop:

  1. Let GtkApplication.run() own it. Standard pattern. Your activate callback builds the UI; run() blocks until the last window closes.
  2. Pump the loop yourself. Useful for embedding GTK in a script that already has its own main loop, or for tests. You call glib.iterate(blocking) in a loop until you decide to stop.

The first example below uses the second approach because it keeps the code short and explicit.

Signals and callbacks

A "signal" is GTK's word for an event your code can listen to. When you click a button, its clicked signal fires. When someone presses Enter in an entry, its activate signal fires. When a window is asked to close, its close-request signal fires.

The Aussom bindings expose each signal through a setOn<Name> method. Point it at a method reference:

btn = new Button("Save");
btn.setOnClicked(::onSaveClicked);

// Aussom callback. Self is the Button wrapper (not a raw pointer).
public onSaveClicked(object Self) {
    c.info("save clicked");
    return null;
}

The first argument your callback receives is the emitter -- the Button that fired the signal -- already wrapped. You do not need to call new Button(rawPointer) yourself. Later arguments depend on the signal; for example GestureClick::pressed delivers (gesture, nPress, x, y).

Layout containers

You rarely add children directly to a window. Instead you add them to a layout container, and set the container as the window's child. The two containers you will use most:

  • Box -- arranges children in a row (Orientation.horizontal) or a column (Orientation.vertical). Add children with append().
  • Grid -- arranges children in a 2D grid. Attach children to a specific column/row/column-span/row-span with attach(child, col, row, w, h).

Other useful containers: ScrolledWindow (scrollable viewport), Stack (one visible child at a time), Notebook (tabbed pages), HeaderBar (title bar with actions), Paned (resizable split pane).

Shape of the Aussom API

Three patterns that recur throughout the bindings.

Primary constructors

Every wrapper class has a single public ClassName(...) constructor. Arguments map to the most common GTK constructor for that type. Examples:

new Window()                               // gtk_window_new
new Button("Submit")                       // gtk_button_new_with_label
new Label("initial")                       // gtk_label_new
new Entry()                                // gtk_entry_new
new Box(Orientation.vertical, 10)          // gtk_box_new
new Image()                                // gtk_image_new

Less common constructors are emitted as static methods on a sibling helper class named <Class>Ctors. For GtkButton's icon and mnemonic variants:

iconBtn = ButtonCtors.newFromIconName("document-open");
mnBtn   = ButtonCtors.newWithMnemonic("_File");

All primary constructors also accept an existing NativeHandle or another wrapper in the first-arg position, which means you can "re-wrap" a widget you have as one type into a different type (details below under Downcast via re-wrap).

Signal setters

Every signal turns into a setOn<Name>(callback, UserData = null) method. The emitter strips the GIR signal name's separators and capitalizes the first letter -- it does not camel-case every word -- so a multi-word signal name like close-request turns into setOnCloserequest, not setOnCloserequest:

Signal Method
clicked setOnClicked
activate setOnActivate
close-request setOnCloserequest
value-changed setOnValuechanged
row-activated setOnRowactivated
notify::property use connectSignal("notify::property", cb)

When in doubt, open the widget's generated .aus file and grep for setOn -- the real names are there.

The callback you pass receives the emitter as its first argument, already wrapped. Later arguments follow the specific signal:

btn = new Button("Submit");
btn.setOnClicked(::onSubmit);

public onSubmit(object Self) {
    // Self is already a Button wrapper. No re-wrapping needed.
    c.info("label is: " + Self.get_label());
    return null;
}

For property-change events (the GObject notify::property family) and the handful of signals that do not have a convenience setter, use the low-level widget.connectSignal("signal-name", callbackWrapper) path where callbackWrapper is a <Class><Signal>Callback instance wrapping your method reference.

Enums are strings

Every GIR enum turns into an Aussom enum whose values are strings. So Orientation.horizontal evaluates to "horizontal" at runtime. Bindings parameters that take an enum are typed string at the signature level; the method body does the string-to-int conversion internally by calling the generated <Name>Enum.toInt(value) helper.

box = new Box(Orientation.vertical, 8);
entry.asWidget().set_halign(Align.center);
label.set_justify(Justification.center);

You can pass a string literal or the enum reference interchangeably:

new Box("horizontal", 4);              // works
new Box(Orientation.horizontal, 4);    // also works, more readable

The enum form is preferred because it is self-documenting and discoverable through the generated docs.

Downcast via re-wrap

GTK widget types form an inheritance hierarchy (a Button IS-A Widget), and several widgets also implement interfaces (an Entry IS-A GtkEditable, a Label IS-A GtkAccessible). The Aussom bindings model both with wrapper classes, which means you sometimes want the same native pointer to be typed as a different wrapper.

Pass the wrapper you have into the primary constructor of the class you want, and it adopts the underlying pointer:

entry = new Entry();
// Entry implements GtkEditable. Wrap as Editable to call
// interface methods without going through .handle() yourself.
editable = new Editable(entry);
editable.set_text("initial");
pos = editable.get_position();

Upcasts use the asX() helpers the bindings emit for each parent class or interface:

btn = new Button("Go");
btn.asWidget().set_sensitive(true);
btn.asAccessible().get_accessible_role();

Common widgets at a glance

One-liners to help you pick the right widget without reading the full GTK docs.

Top-level:

  • Window -- a plain top-level window. Use set_child() to add the root of your UI tree.
  • ApplicationWindow -- a Window that is aware of its parent Application and can automatically add its actions to the app's action map. Use with Application.

Layout:

  • Box(Orientation, Spacing) -- pack children in a row or column. append(child) adds to the end, prepend(child) to the front.
  • Grid -- 2D layout. attach(child, col, row, w, h) places a child spanning w columns and h rows.
  • ScrolledWindow -- wraps one child and makes it scrollable. set_child(bigContent) then place the scrolled window in the tree.
  • HeaderBar -- a title bar with a centered title and room for action buttons. Typical modern-GTK app layout.
  • Stack -- holds multiple children, only one visible at a time. Useful for tabbed or wizard UIs without visible tabs.
  • Notebook -- Stack with visible tabs.
  • Paned -- resizable two-pane split.
  • Popover -- a transient floating panel attached to another widget (menus, autocomplete, etc.).

Text and display:

  • Label(Text) -- displays text. Supports Pango markup through set_markup().
  • Image / ImageCtors.newFromIconName(...) -- icon or image display.
  • Picture -- scalable image that resizes with its allocation.

Input:

  • Entry -- single-line text input. Implements GtkEditable so you can re-wrap it to get/set text and caret position.
  • TextView -- multi-line text editor. Uses a TextBuffer for its content.
  • SpinButton -- numeric up/down picker.
  • Scale -- slider. Orientation + adjustment.
  • Switch -- on/off toggle.

Actions and choices:

  • Button(Label) -- simple push button.
  • ToggleButton -- button that stays pressed or not.
  • CheckButton(Label) -- checkbox with label.
  • DropDown -- single-select dropdown backed by a GListModel.
  • ComboBoxText -- simpler string-only dropdown.
  • MenuButton -- button that opens a Popover or menu.

Lists:

  • ListBox -- simple vertical list of rows. Good default when you do not need virtualization.
  • ListView -- virtualized list for thousands of rows. Works with a GtkListItemFactory + GListModel.

Dialogs and messaging:

  • AlertDialog -- modern replacement for the deprecated MessageDialog.
  • FileDialog -- modern replacement for FileChooserDialog.
  • Prefer a plain Window with set_modal(true) + set_transient_for(parent) when you need a custom popup. The GtkDialog base class is deprecated in GTK4.10+ and its content area does not always map under modern compositors.

Every widget class has a much richer surface than one line can capture. The generated Aussom docs mirror the GTK reference; for a type named Foo, open the corresponding Foo.aus file or the generated markdown at docs/markdown/gtk/....

Example 1 -- Hello World

Minimum viable app: one window, one label, its own main loop.

include panama;
include gtk.gtk.functions;
include gtk.gtk.window;
include gtk.gtk.label;

class helloWorldApp {
    public gtkApi = null;
    public mainIter = null;
    public running = true;
    public window = null;

    public helloWorldApp() {
        this.gtkApi = new GtkApi();
        glibLib = panama.openLibrary("libglib-2.0.so.0");
        this.mainIter = glibLib.bind("g_main_context_iteration",
                panama.funcType("int", ["pointer", "int"]));
    }

    public run() {
        this.gtkApi.init();

        this.window = new Window();
        this.window.set_title("Hello");
        this.window.set_default_size(320, 120);
        this.window.setOnCloserequest(::onClose);
        this.window.set_child(new Label("Hello, world."));
        this.window.present();

        while (this.running) {
            this.mainIter.call(panama.nullHandle("GMainContext*"), 1);
        }
        return null;
    }

    public onClose(object Self) {
        this.running = false;
        return false;
    }
}

class Main {
    public main(args) {
        app = new helloWorldApp();
        app.run();
        return null;
    }
}

Points to notice:

  • GtkApi().init() initializes GTK. Do this once before creating widgets.
  • setOnCloserequest(::onClose) wires up a callback. The Self parameter is the Window wrapper.
  • Returning false from onClose lets the window actually close; returning true would suppress the close.
  • The manual while(running) loop pumps the GLib main context, blocking until an event arrives. Application.run() does this for you; the manual pump shows what is happening under the hood.

Example 2 -- Interactive form

A form with a text field, a button, and a result label. Clicking the button copies the entry's text into the label.

include panama;
include gtk.gtk.functions;
include gtk.gtk.window;
include gtk.gtk.box;
include gtk.gtk.label;
include gtk.gtk.entry;
include gtk.gtk.button;
include gtk.gtk.editable;
include gtk.gtk.enums;

class formApp {
    public gtkApi = null;
    public mainIter = null;
    public running = true;
    public window = null;
    public nameEntry = null;
    public greetLabel = null;

    public formApp() {
        this.gtkApi = new GtkApi();
        glibLib = panama.openLibrary("libglib-2.0.so.0");
        this.mainIter = glibLib.bind("g_main_context_iteration",
                panama.funcType("int", ["pointer", "int"]));
    }

    public run() {
        this.gtkApi.init();
        this.build();
        this.window.present();
        while (this.running) {
            this.mainIter.call(panama.nullHandle("GMainContext*"), 1);
        }
        return null;
    }

    public build() {
        this.window = new Window();
        this.window.set_title("Greeter");
        this.window.set_default_size(360, 160);
        this.window.setOnCloserequest(::onClose);

        content = new Box(Orientation.vertical, 10);

        prompt = new Label("Enter your name:");
        content.append(prompt);

        this.nameEntry = new Entry();
        content.append(this.nameEntry);

        btn = new Button("Greet");
        btn.setOnClicked(::onGreet);
        content.append(btn);

        this.greetLabel = new Label("");
        content.append(this.greetLabel);

        this.window.set_child(content);
        return null;
    }

    public onGreet(object Self) {
        name = new Editable(this.nameEntry).get_text();
        if (name == null || name == "") {
            this.greetLabel.set_text("(type a name first)");
        } else {
            this.greetLabel.set_text("Hello, " + name + "!");
        }
        return null;
    }

    public onClose(object Self) {
        this.running = false;
        return false;
    }
}

class Main {
    public main(args) {
        app = new formApp();
        app.run();
        return null;
    }
}

Points to notice:

  • Widgets are created, composed into a Box, and the Box is set as the window's child.
  • setOnClicked wires the button to onGreet. When the user clicks, Self is the Button, but we do not need it here; we reach into the app's state (this.nameEntry, this.greetLabel) directly.
  • new Editable(this.nameEntry) is the downcast pattern. Entry implements the GtkEditable interface; re-wrapping gives you the interface methods (get_text, set_text, get_position, select_region) without going through raw native handles.

Example 3 -- Grid calculator

A 3x3 button grid plus a result label. Shows Grid layout and a single callback shared across multiple buttons via their label.

include panama;
include gtk.gtk.functions;
include gtk.gtk.window;
include gtk.gtk.grid;
include gtk.gtk.box;
include gtk.gtk.label;
include gtk.gtk.button;
include gtk.gtk.enums;

class calcApp {
    public gtkApi = null;
    public mainIter = null;
    public running = true;
    public window = null;
    public display = null;
    public expr = "";

    public calcApp() {
        this.gtkApi = new GtkApi();
        glibLib = panama.openLibrary("libglib-2.0.so.0");
        this.mainIter = glibLib.bind("g_main_context_iteration",
                panama.funcType("int", ["pointer", "int"]));
    }

    public run() {
        this.gtkApi.init();
        this.build();
        this.window.present();
        while (this.running) {
            this.mainIter.call(panama.nullHandle("GMainContext*"), 1);
        }
        return null;
    }

    public build() {
        this.window = new Window();
        this.window.set_title("Calc");
        this.window.set_default_size(240, 280);
        this.window.setOnCloserequest(::onClose);

        root = new Box(Orientation.vertical, 6);

        this.display = new Label("");
        this.display.asWidget().set_hexpand(true);
        this.display.asWidget().add_css_class("calc-display");
        root.append(this.display);

        grid = new Grid();
        grid.set_row_spacing(4);
        grid.set_column_spacing(4);

        // layout: 3x3 number pad plus a row for "C" and "=".
        keys = [
            ["1", 0, 0], ["2", 1, 0], ["3", 2, 0],
            ["4", 0, 1], ["5", 1, 1], ["6", 2, 1],
            ["7", 0, 2], ["8", 1, 2], ["9", 2, 2],
            ["C", 0, 3], ["0", 1, 3], ["=", 2, 3]
        ];
        for (key : keys) {
            btn = new Button(key[0]);
            btn.asWidget().set_hexpand(true);
            btn.setOnClicked(::onKey);
            grid.attach(btn, key[1], key[2], 1, 1);
        }
        root.append(grid);

        this.window.set_child(root);
        return null;
    }

    public onKey(object Self) {
        label = Self.get_label();
        if (label == "C") {
            this.expr = "";
        } else if (label == "=") {
            // Pretend evaluator: reverse the expression so the
            // example is self-contained.
            this.expr = this.reverse(this.expr);
        } else {
            this.expr += label;
        }
        this.display.set_text(this.expr);
        return null;
    }

    public reverse(string Text) {
        out = "";
        parts = Text.split("");
        i = #parts - 1;
        while (i >= 0) {
            out += parts[i];
            i -= 1;
        }
        return out;
    }

    public onClose(object Self) {
        this.running = false;
        return false;
    }
}

class Main {
    public main(args) {
        app = new calcApp();
        app.run();
        return null;
    }
}

Points to notice:

  • Grid.attach(child, col, row, w, h) is the GTK4 way. A single cell uses (col, row, 1, 1).
  • Every button shares the same ::onKey callback. The callback tells the buttons apart by calling Self.get_label(). Self is the emitter wrapper, already typed as Button in the signal trampoline.
  • The app keeps its own state (this.expr) in an Aussom field; widgets just render the current value.

Example 4 -- List with selection

A ListBox with one row per option. Selecting a row updates a label. Shows filling a container from a list, a row-selected signal callback, and reading the ListBoxRow index back out.

include panama;
include gtk.gtk.functions;
include gtk.gtk.window;
include gtk.gtk.box;
include gtk.gtk.label;
include gtk.gtk.listbox;
include gtk.gtk.listboxrow;
include gtk.gtk.enums;

class listApp {
    public gtkApi = null;
    public mainIter = null;
    public running = true;
    public window = null;
    public list = null;
    public result = null;
    public choices = ["Chromium", "Firefox", "WebKit"];

    public listApp() {
        this.gtkApi = new GtkApi();
        glibLib = panama.openLibrary("libglib-2.0.so.0");
        this.mainIter = glibLib.bind("g_main_context_iteration",
                panama.funcType("int", ["pointer", "int"]));
    }

    public run() {
        this.gtkApi.init();
        this.build();
        this.window.present();
        while (this.running) {
            this.mainIter.call(panama.nullHandle("GMainContext*"), 1);
        }
        return null;
    }

    public build() {
        this.window = new Window();
        this.window.set_title("Picker");
        this.window.set_default_size(320, 240);
        this.window.setOnCloserequest(::onClose);

        content = new Box(Orientation.vertical, 8);
        content.append(new Label("Pick a browser:"));

        this.list = new ListBox();
        for (item : this.choices) {
            this.list.append(new Label(item));
        }
        this.list.setOnRowselected(::onRowSelected);
        content.append(this.list);

        this.result = new Label("(nothing selected)");
        content.append(this.result);

        this.window.set_child(content);
        return null;
    }

    public onRowSelected(object Self, object Row) {
        // Row is a ListBoxRow wrapper. It can be null-handle when a
        // previous selection is cleared, so guard with isNull().
        if (Row == null || Row.isNull()) {
            this.result.set_text("(nothing selected)");
            return null;
        }
        idx = Row.get_index();
        this.result.set_text("You picked " + this.choices[idx]);
        return null;
    }

    public onClose(object Self) {
        this.running = false;
        return false;
    }
}

class Main {
    public main(args) {
        app = new listApp();
        app.run();
        return null;
    }
}

Points to notice:

  • ListBox.append(widget) takes any widget. If the widget is not already a ListBoxRow, GTK wraps it in one for you. The index of that row matches the order of append calls.
  • setOnRowselected delivers (listBox, row) to your callback, with both arguments already wrapped. You do not need to convert the raw row pointer yourself.
  • For thousands of rows, prefer ListView with a SingleSelection and a SignalListItemFactory. ListBox is the right default for modest-sized lists because every row is a real widget.
  • DropDown (backed by a GListModel) is a good choice when you want a compact picker rather than an expanded list. Its typical selection signal is the property notification notify::selected, which does not have a convenience setter -- use widget.connectSignal("notify::selected", callbackWrapper) for that path.

Running your app

Most GTK apps launch like any Aussom script:

aussom my-app.aus

Under a terminal-launched test or CLI session on Wayland, the compositor may not always send frame events to your window because it does not hold keyboard focus. This can make the frame clock stall and, in turn, make gtk_test_widget_wait_for_draw (used by gtktest.capture and gtktest.startRecording) hang. If you run into this, force GTK onto XWayland with the X11 backend:

GDK_BACKEND=x11 aussom my-app.aus

This matches the advice in gtktest-usage.md for test runs and is generally the safer default when you need deterministic frame delivery.

For interactive apps launched from a desktop environment (double click or menu entry) this problem does not occur; the compositor hands the window focus as part of the launch and frame events flow normally.

Regenerating the bindings

The GTK4 wrappers are generated from GIR metadata by the aussom-gi emitter. You will only need to regenerate them if you upgrade GTK, change the emitter, or add a new namespace. The full command:

aussom tools/aussom-gi/aussom-gi.aus \
    --gir gtk/gir-in/GLib-2.0.gir \
    --gir gtk/gir-in/GObject-2.0.gir \
    --gir gtk/gir-in/GModule-2.0.gir \
    --gir gtk/gir-in/Gio-2.0.gir \
    --gir gtk/gir-in/HarfBuzz-0.0.gir \
    --gir gtk/gir-in/freetype2-2.0.gir \
    --gir gtk/gir-in/cairo-1.0.gir \
    --gir gtk/gir-in/Pango-1.0.gir \
    --gir gtk/gir-in/PangoCairo-1.0.gir \
    --gir gtk/gir-in/GdkPixbuf-2.0.gir \
    --gir gtk/gir-in/Graphene-1.0.gir \
    --gir gtk/gir-in/Gdk-4.0.gir \
    --gir gtk/gir-in/Gsk-4.0.gir \
    --gir gtk/gir-in/Gtk-4.0.gir \
    --out src/com/lehman/aussom/stdlib/aus/gtk \
    --module gtk \
    --emit aus \
    --lib-strategy cross-platform

See design/aussom-gi-idiomatic-api.md for the emitter design.

Where to go next

  • gtktest-usage.md -- sibling guide for UI testing, including selectors, semantic click, capture, and recording.
  • Generated API docs under docs/markdown/gtk/ and docs/html/gtk/ -- one page per widget class with every method and signal. Use this as your reference when you need a method you do not remember.
  • Official GTK4 tutorial at https://docs.gtk.org/gtk4/ -- the upstream docs. The concepts carry over exactly; only the calling syntax differs from the Aussom bindings you see here.
  • design/aussom-gi-idiomatic-api.md -- the design doc for the idiomatic Aussom API (primary constructors, signal trampolines, enum strings, container iteration). Useful if you want to understand why the bindings look the way they do.