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.
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.
Before any code, here are the concepts that recur in every GTK app.
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 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);
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.
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:
GtkApplication.run() own it. Standard pattern. Your
activate callback builds the UI; run() blocks until the last
window closes.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.
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).
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).
Three patterns that recur throughout the bindings.
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).
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.
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.
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();
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.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/....
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.false from onClose lets the window actually close;
returning true would suppress the close.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.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:
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.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).::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.this.expr) in an Aussom field;
widgets just render the current value.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.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.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.
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.
gtktest-usage.md -- sibling guide for UI testing, including
selectors, semantic click, capture, and recording.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.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.