Note: This project is no longer actively maintained and my attention has moved to the community project gtk-kn.
Write GTK applications using Kotlin/Native.
This project provides binding modules for GTK4 and related GNOME libraries combined with utility modules that make it possible to write GTK desktop applications in idiomatic Kotlin.
This project does not provide bindings for the full GTK API surface and all of its GNOME dependencies.
The main goal is to provide enough glue for application developers familiar with Kotlin to get started writing GTK applications without having to dig into the details of the C API.
While this project aims to wrap/abstract the existing C API into an idiomatic(-ish) Kotlin API, it does not hide the underlying C API from developers. When a Kotlin version of a type, method or function is not available, the original C version can still be used.
This is an experimental holiday hacking project, mostly for learning purposes. I started this project because I wanted to experiment with writing GTK applications using Kotlin/Native.
While the gtk-kt project already provides bindings for various GNOME libraries including Gtk4 and libadwaita, I decided to start from scratch as a learning experience and deep-dive into both Kotlin/Native and GTK.
My endgame is a task/calendar/note application that I want to make available on desktop and mobile platforms while using as much shared code as possible. The idea is to write the shared code as a Kotlin multiplatform project and on top of that use GTK for desktop platforms, UIKit with Kotlin/Native for iOS and Kotlin for Android.
This repository contains one multi-module Gradle project containing modules for library bindings and modules for extensions and utility libraries.
Module | Description |
---|---|
gobject-bindings | Kotlin bindings for GObject. |
gio-bindings | Kotlin bindings for GIO. |
gtk-bindings | Kotlin bindings for GTK4. |
libadwaita-bindings | Kotlin bindings for libadwaita (target version 1.2). |
A hello world example using only GTK.
import bindings.gtk.Application
import bindings.gtk.ApplicationWindow
import bindings.gtk.Label
fun main() {
val app = Application("com.example.app")
app.onActivate {
val window = ApplicationWindow(app)
window.title = "Hello World"
window.defaultSize = Pair(600, 400)
window.child = Label("Hello from Kotlin/Native")
window.show()
}
app.runApplication()
app.unref()
}
A counter example using only GTK.
fun main() {
val app = Application("com.example.counter")
var counter = 0
app.onActivate {
// create the window
val window = ApplicationWindow(app)
window.title = "Hello World"
window.defaultSize = Pair(600, 400)
// create the widgets
val label = Label()
val button = Button("Click me")
// attach a button click handler
button.onClicked {
label.text = "You clicked ${++counter} times"
}
// wrap in a box for layout
window.child = Box(GTK_ORIENTATION_VERTICAL, 20).apply {
valign = GTK_ALIGN_CENTER
append(button)
append(label)
}
// show the window
window.show()
}
app.runApplication()
app.unref()
}
Other examples:
- TreeListModel
- ListModel
- GridView
- Custom Widget subclass and property binding
- 7GUIs - #1 Counter
- 7GUIs - #2 Temperature Converter
- 7GUIs - #3 Flight Booker
See the examples folder for more examples.
Currently building this project requires fairly recent versions of its dependencies. I'll try to get the required dependency versions down a little in the near future.
- gio >= 2.74
- gtk4 >= 4.6.6
- libadwaita-1 >= 1.2 (optional, only when using libadwaita-bindings)
Ubuntu dependencies
apt install libgtk4-dev gcc-multilib
Arch dependencies
pacman -S gtk4 libadwaita
Building the full project (requires libadwaita >= 1.2)
./gradlew build -x test
Building only the gtk bindings
./gradlew gtk-bindings:build -x test
The project can be built on Windows in the Mingw64 environment.
- Download and install Mingw64
- Install dependencies in the mingw64 shell
pacman -S base-devel mingw-w64-x86_64-toolchain mingw-x86_64-gtk4 mingw-w64-x86_64-libadwaita
- Trigger the build
./gradlew build -x test
This project is not yet published to a repository. The best way to get started is to publish to your local Maven repository.
./gradlew publishToMavenLocal -x test
And then in your own project build.gradle.kts file:
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
implementation("io.quantus.gtk-kotlin-native:gtk-bindings:0.0.1-SNAPSHOT")
implementation("io.quantus.gtk-kotlin-native:libadwaita-bindings:0.0.1-SNAPSHOT")
}
This project is still highly experimental. There should be enough ported/wrapped to write simple applications.
Basic application, window and widget creation is available. As well as simple property binding, signal handling and working with actions.
With these basics available, I'm focussing first on getting most of libadwaita wrapped since that provides most of the building blocks for modern UI applications (preferences, action rows, headers, dialogs, toasts, style classes,...).
After that I'm working on supporting more of GTK (more widgets, shortcuts, search, text entry, file/color/font chooser dialogs,...) and some parts of GIO (lists, actions, icons, files).
gobject-bindings
module- basic
GObject
support - basic
GVariant
support - basic property binding support
- experimental support for subclassing
GObject
in user-defined types.
- basic
gio-bindings
module- Application
- Actions
- Action
- SimpleAction
- ActionMap
- Lists
- ListModel
- ListStore
- Menus
- Menu
- MenuItem
- MenuModel
gtk-bindings
module- Application and Windows
- Application
- Window
- ApplicationWindow
- PreferencesWindow
- ShortcutsWindow
- Widgets
- Widget
- Label
- Button
- Box
- DropDown
- ListBox
- ListBoxRow
- MenuButton
- Stack
- StackPage
- StackSidebar
- ToggleButton
- AspectFrame
- CenterBox
- CheckButton
- Grid
- GridView
- HeaderBar
- Image
- LevelBar
- LinkButton
- ListView
- LockButton
- Popover
- ProgressBar
- ScrolledWindow
- SearchBar
- Separator
- Switch
- Dialogs
- AboutDialog
- MessageDialog
- Lists and Trees
- StringList
- ListItem
- ListItemFactory
- SignalListItemFactory
- TreeExpander
- TreeListModel
- TreeListRow
- Interfaces
- Actionable
- Editable
- Native
- Orientable
- Root
- SelectionModel
- Application and Windows
libadwaita-bindings
module- Application and Windows
- Application
- Window
- ApplicationWindow
- AboutWindow
- Widgets
- ActionRow
- Avatar
- ButtonContent
- ComboRow
- Clamp
- EntryRow
- ExpanderRow
- Flap
- HeaderBar
- Leaflet
- LeafletPage
- PasswordEntryRow
- StatusPage
- TabView
- TabBar
- TabPage
- Message Dialogs
- Preferences
- PreferencesGroup
- PreferencesRow
- PreferencesPage
- PreferencesWindow
- Toasts
- ToastOverlay
- Toast
- ViewStack
- ViewStack
- ViewStackPage
- ViewSwitcher
- ViewSwitcherBar
- ViewSwitcherTitle
- Application and Windows
Some things I would like to experiment with in the near future:
A coroutines-gtk
module that adds coroutine support providing Dispatchers.Gtk
context and Dispatcher.Main
implementation.
A set of extension properties providing CoroutineScopes
for Application
, Window
and Widget
.
I'm thinking something similar to how Android provides viewModelScope
and lifecycleScope
properties.
windowScope.launch {
// Coroutine that is cancelled when the window closes.
}
The current interface bindings (like ListModel
for GListModel
) don't have any type information. This means getting an
item from a ListModel
will always return an Object
instance which the user needs to downcast to the expected type.
I want to experiment with adding type parameters to the bindings so that when a method returns a model for which the return
type is known, it can return a ListModel<SomeType>
.
Find a better way to create GObject subclasses and create custom widgets.
See Deriving custom widget classes and Deriving custom object classes with properties
Currently it builds on Linux and Window. macOS support is on my list.
While GObject Introspection - GIR provides a way to extract type information from GObject-based libaries, I decided to not use it for now and write the first version manually. Writing the bindings manually forces me to look at the GObject API in detail (which I had not worked with before), and allows me to iterate fast on various approaches.
When the library matures and I have a few applications under my belt, I should have a better view of what the bindings should look like, what works and what does not, and then I will have another look at automated GIR-based bindings.
Find a clean way to integrate GLib reference counting.
There is already a WIP branch with webkit2gtk-5.0 support.
GtkSourceView is another library I would like to add in the near future.
Most libraries in the GNOME family are based on GObject which is an object-oriented type system written in C.
The binding modules in this project wrap the native C types into Kotlin classes using the following conventions.
The binding modules use Kotlin/Native cinterop
to import C headers into a kotlin package prefixed with native.<modulename>
.
native.gobject
native.gio
native.gtk
The binding modules provide wrapped classes in a kotlin packages prefixed with bindings.<modulename>
.
bindings.gobject
bindings.gio
bindings.gtk
Wrapped classes and interfaces are named after their native type without prefix.
For example the Kotlin class wrapping the native GtkButton
type is named Button
and resides in the bindings.gtk
package in the gtk-bindings
module.
- Wrapped classes are wrappers around a C pointer to the wrapped object and don't hold any state in the class instance.
- Wrapped class implementations delegate most of the work to C methods for the wrapped type.
- Every class extends
bindings.gobject.Object
(or subclass) which holds aCPointer<GObject>
namedgPointer
pointing to the underlying nativeGObject
. - Every class exposes a typed pointer val named after their native type which casts the underlying
gPointer
to a typed pointer. - Wrapped classes typically don't have a primary constructor.
- Wrapped classes have a constructor taking a generic
CPointer<*>
and pass the pointer up to their parent class constructor. - Wrapped classes that can be instantiated also have a no-arg constructor or one or more utility constructors. These constructors
typically call a native C
_new()
function (for examplegtk_label_new()
) and pass the resulting pointer to their super class. - Wrapped classes can have Kotlin properties (var/val) and methods which should all delegate to native functions passing in the typed pointer as the first argument.
Wrapper classes look like this:
package bindings.gtk
// class named Button, wraps a GtkButton
// extends Widget, wraps a GtkWidget
// implements Actionable interface, wraps GtkActionable
open class Button : Widget, Actionable {
// expose typed pointer named gtkButtonPointer, casted down from the underlying gPointer
val gtkButtonPointer get() = gPointer.asTypedPointer<GtkButton>()
// expose typed pointer required by the Actionable interface
override val gtkActionablePointer get() = gPointer.asTypedPointer<GtkActionable>()
// constructor taking a pointer and delegating to super
// used for wrapping pointers into a class instance
constructor(pointer: CPointer<*>) : super(pointer)
// no-arg constructor for instantiating new objects of this class
constructor() : this(gtk_button_new()!!)
// utility constructor for instantiating using an alternative function
constructor(label: String) : this(gtk_button_new_with_label(label)!!)
// a readwrite kotlin property named label
// delegates the implementation to native C functions
// - passes the typed pointer as the first argument
// - performs argument and return type conversion
var label: String?
get() = gtk_button_get_label(gtkButtonPointer)?.toKString()
set(value) = gtk_button_set_label(gtkButtonPointer, value)
// a readwrite kotlin property named child
// delegates the implementation to native C functions
// - passes the typed pointer as the first argument
// - performs argument and return type conversion
var child: Widget?
// this C function returns a widget pointer, convert to a wrapped Widget class using the asWidget extension function
get() = gtk_button_get_child(gtkButtonPointer)?.asWidget()
// this C function takes a widget pointer as argument, extract the typed pointer from the wrapped Widget instance
set(value) = gtk_button_set_child(gtkButtonPointer, value?.gtkWidgetPointer)
/* ...various other properties and methods... */
}
Native interfaces are wrapped as well. Each native interface is wrapped in a Kotlin interface.
- Each wrapped interface has a typed pointer val named after the native interface type. Every class implementing the interface is required to provide this pointer.
- Wrapped interfaces can have Kotlin properties (var/val) and methods which should all delegate to native functions passing in the typed pointer as the first argument.
Wrapper interfaces look like this:
package bindings.gtk
// interface named Orientable, wraps the GtkOrientable interface
interface Orientable {
// require implementing class to provide a typed pointer to the underlying object
val gtkOrientablePointer: CPointer<GtkOrientable>
// a readwrite kotlin property named orientation
var orientation: GtkOrientation
get() = gtk_orientable_get_orientation(gtkOrientablePointer)
set(value) = gtk_orientable_set_orientation(gtkOrientablePointer, value)
}
Deriving custom widgets is currently possible with limited support for actions and signals. (properties coming soon)
- Declare a Kotlin class that extends
Widget
(or another class subclassed from Widget). - Add a (primary) constructor that takes a
CPointer<*>
argument and pass the pointer tosuper
constructor. - Add a companion object that extends
WidgetCompanion<T>
whereT
is your class.- Override the
typeName
property with the name for your class. - Override the
parentType
property with theType
object from your parent class. - Override
classInit
and install any signals, actions or properties your widget provides.
- Override the
- Add a secondary constructor that calls
newInstancePointer()
from your companion object and pass the resulting pointer to the primary constructor. - (optional) The secondary constructor block can assign instance properties from your constructor arguments.
An example for a custom widget named MyWidget
that extends the libadwaita Bin
widget and provides an action and a signal:
class MyWidget(pointer: CPointer<*>) : Bin(pointer) {
var name: String = ""
constructor(name: String) : this(newInstancePointer()) {
this.name = name
val button = Button(name)
button.actionName = "mywidgetbutton-clicked"
this.child = button
}
private fun myButtonClickedAction() {
emitSignal(TEST_SIGNAL)
}
companion object : WidgetCompanion<MyWidget>() {
override val typeName = "MyWidget"
override val parentType = Bin.Type
const val TEST_SIGNAL = "test-signal"
override fun classInit(klass: WidgetClass<MyWidget>) {
// install the signal
klass.installSignal(TEST_SIGNAL, G_SIGNAL_RUN_FIRST)
// install the action with the handler
klass.installAction("mywidgetbutton-clicked", MyWidget::myButtonClickedAction)
}
}
}
- Declare a Kotlin class that extends
Object
(or another class subclasses from Object). - Add a (primary) constructor that takes a
CPointer<*>
argument and pass the pointer tosuper
constructor. - Add a companion object that extends
ObjectCompanion<T>
whereT
is your class.- Override the
typeName
property with the name for your class. - Override the
parentType
property with theType
object from your parent class. - Override
classInit
and install any signals or properties your object provides.
- Override the
- Add a secondary constructor that calls
newInstancePointer()
from your companion object and pass the resulting pointer to the primary constructor. - (optional) The secondary constructor block can assign instance properties from your constructor arguments.
An example for a custom object class named MyPerson
with some properties:
private class MyPerson : Object {
constructor(pointer: CPointer<*>) : super(pointer)
constructor() : this(newInstancePointer())
var name: String by NAME_PROPERTY
var surname: String? by SURNAME_PROPERTY
var age: Int by AGE_PROPERTY
companion object : ObjectCompanion<MyPerson>() {
override val typeName = "MyPerson"
override val parentType = Object.Type
private val NAME_PROPERTY = stringProperty(MyPerson::name, "name", defaultValue = "")
private val SURNAME_PROPERTY = nullableStringProperty(MyPerson::surname, "surname", defaultValue = null)
private val AGE_PROPERTY = intProperty(MyPerson::age, "age", "age", "A persons age", 0, 200, 0)
override fun classInit(klass: ObjectClass<MyPerson>) {
klass.installProperty(NAME_PROPERTY)
klass.installProperty(SURNAME_PROPERTY)
klass.installProperty(AGE_PROPERTY)
}
}
}
With the MyPerson
class defined like this, you can instantiate it as you would with any regular Kotlin class and it will
act as a GObject.
- Assigning a value to the
name
,surname
andage
properties of the kotlin instance will update the instance property as well as notify on the GObject for the property. - Assigning a value to a property by name using external g_object methods (for example through property binding) will update the instance properties.
val person = MyPerson()
person.name = "Steven" // notifies on name property
person.age = 35 // notifies on age property
person.setProperty("age", 19) // update property through gobject
println(person.age) // prints 19
// property binding works as expected
val anotherPerson = MyPerson()
person.bindProperty("age", anotherPerson, "age")
person.age = 42
println(anotherPerson.age) // prints 42
Some GTK API's (for example list models or list item factories) return a pointer to a GObject
which this library wraps in
an instance of the Object
class. These instances cannot be simple downcasted to a user-defined class using the Kotlin myobject as MyWidget
syntax.
Instead, these objects should be converted back to a target class instance by using the asType
extension on Object
.
val task = row.item.asType(Task.Type) // Throws on type error
// or
val task = row.item.asTypeOrNull(Task.Type) // null on type error
The Type
property is available on every wrapped class and on every user-defined class that implements the ObjectCompanion
or WidgetCompanion
companion object.
See the TreeListModel example for more examples.