Skip to content

Kotlin/Native interop to libui: a portable GUI library

License

Notifications You must be signed in to change notification settings

msink/kotlin-libui

Repository files navigation

kotlin-libui

Kotlin/Native bindings to the libui C library.

Build Status Build status

libui is a C lightweight multi-platform UI library using native widgets on Linux (Gtk3), macOS, and Windows. Using this bindings you can develop cross-platform but native-looking GUI programs, written in Kotlin, and compiled to small native executable file.

Using

To use this library in your project you can clone Hello World application and use it as starting point.

Building

Cross-platform build is automated using Travis for Linux and macOS targets, and AppVeyor for Windows targets. Just create release on GitHub, and executable files for all 3 major desktop platforms will be compiled and attached to release.

For local build use ./gradlew build on Linux or macOS, or gradlew build on Windows. In this case only one - native for your platform - file will be built.

The script below builds kotlin-libui then builds and runs the sample hello-ktx:

#clone this project
git clone https://github.com/msink/kotlin-libui.git

#build libui.klib
cd kotlin-libui/libui
../gradlew build

#build and run the hello-ktx sample
cd ../samples/hello-ktx/
../../gradlew run

You can use IntelliJ IDEA CE/UE or CLion EAP for code navigation and code completion, debugging works only in CLion.

Status

Warning: currently it is just a prototype - works in most cases, but not protected from errors. And as both libui and Kotlin/Native are currently in alpha stage, anything can change.

Well, I'm also not sure about DSL syntax - it works, and for now is good enough. Let's leave it as is for a while.

If anyone have ideas - Issues and PullRequests are welcome.

Hello World

Let's start from minimal sample application - single button and single scrollable text area.

Screenshots:

Windows

Unix

macOS


C implementation:
#include "ui.h"

static int onClosing(uiWindow *window, void *data)
{
    uiQuit();
    return 1;
}

static void saySomething(uiButton *button, void *data)
{
    uiMultilineEntryAppend(uiMultilineEntry(data),
        "Hello, World!  Ciao, mondo!\n"
        "Привет, мир!  你好,世界!\n\n");
}

int main(void)
{
    uiInitOptions options;
    uiWindow *window;
    uiBox *box;
    uiButton *button;
    uiMultilineEntry *scroll;

    memset(&options, 0, sizeof(options));
    if (uiInit(&options) != NULL)
        abort();

    window = uiNewWindow("Hello", 320, 240, 0);
    uiWindowSetMargined(window, 1);

    box = uiNewVerticalBox();
    uiBoxSetPadded(box, 1);
    uiWindowSetChild(window, uiControl(box));

    scroll = uiNewMultilineEntry();
    uiMultilineEntrySetReadOnly(scroll, 1);

    button = uiNewButton("libui говорит: click me!");
    uiButtonOnClicked(button, saySomething, scroll);
    uiBoxAppend(box, uiControl(button), 0);

    uiBoxAppend(box, uiControl(scroll), 1);

    uiWindowOnClosing(window, onClosing, NULL);
    uiControlShow(uiControl(window));
    uiMain();
    return 0;
}

Direct translation to Kotlin:
import kotlinx.cinterop.*
import libui.*

fun main(args: Array<String>) = memScoped {
    val options = alloc<uiInitOptions>()
    val error = uiInit(options.ptr)
    if (error != null) throw Error("Error: '${error.toKString()}'")

    val window = uiNewWindow("Hello", 320, 240, 0)
    uiWindowSetMargined(window, 1)

    val box = uiNewVerticalBox()
    uiBoxSetPadded(box, 1)
    uiWindowSetChild(window, box?.reinterpret())

    val scroll = uiNewMultilineEntry()
    uiMultilineEntrySetReadOnly(scroll, 1)
    val button = uiNewButton("libui говорит: click me!")
    fun saySomething(button: CPointer<uiButton>?, data: COpaquePointer?) {
        uiMultilineEntryAppend(data?.reinterpret(),
            "Hello, World!  Ciao, mondo!\n" +
            "Привет, мир!  你好,世界!\n\n")
    }
    uiButtonOnClicked(button, staticCFunction(::saySomething), scroll)
    uiBoxAppend(box, button?.reinterpret(), 0)
    uiBoxAppend(box, scroll?.reinterpret(), 1)

    fun onClosing(window: CPointer<uiWindow>?, data: COpaquePointer?): Int {
        uiQuit()
        return 1
    }
    uiWindowOnClosing(window, staticCFunction(::onClosing), null)
    uiControlShow(window?.reinterpret())
    uiMain()
    uiUninit()
}

While this works, it's far from idiomatic Kotlin.

OK, let's wrap all that noisy function calls, with final goal to get something similar to TornadoFX:

import libui.*

fun main(args: Array<String>) = appWindow(
    title = "Hello",
    width = 320,
    height = 240
) {
    vbox {
        lateinit var scroll: TextArea

        button("libui говорит: click me!") {
            action {
                scroll.append("""
                    |Hello, World!  Ciao, mondo!
                    |Привет, мир!  你好,世界!
                    |
                    |""".trimMargin())
            }
        }
        scroll = textarea {
            readonly = true
            stretchy = true
        }
    }
}

More samples

Documentation

See autogenerated documentation, samples and comments in source code.

Lifecycle management

Kotlin memory management differs from native C model, so all libui objects are wrapped in Kotlin objects inherited from Disposable, and direct using of libui functions is not recommended in most cases.

Disposable objects must be disposed by calling dispose() method, before program ends. Most objects are attached as a child to some other object, in this case parent is responsible to dispose all its children, recursively. As DSL builders automatically add created object to some container - in most cases you do not have to worry about lifecycle management. But if you want to do something not supported by DSL builders - you can create Disposable object directly, and in this case you are responsible to dispose or attach it at some point.