Skip to main content

Python bindings for libui-ng

Project description

python-libui-ng

PyPI Version Python License: MIT GitHub Issues

Native GUI toolkit for Python. Lightweight bindings for libui-ng — real native widgets on Linux (GTK+3), macOS (Cocoa), and Windows (Win32).

No electron. No web views. Just native controls.

Features

  • 30+ native widgets — buttons, entries, sliders, tables, color pickers, drawing surfaces, and more
  • Declarative API — reactive state, composable components, two-way data binding
  • Imperative API — direct low-level control when you need it
  • Async-first — built-in asyncio integration with thread-safe UI updates
  • Cross-platform — one codebase, native look and feel everywhere

Quick Start

pip install libui

Hello World

import libui
from libui.declarative import App, Window, VBox, Label, Button, State

async def main():
    app = App()
    count = State(0)

    app.build(window=Window(
        "Hello", 400, 300,
        child=VBox(
            Label(text=count.map(lambda n: f"Count: {n}")),
            Button("Click me", on_clicked=lambda: count.update(lambda n: n + 1)),
        ),
    ))

    app.show()
    await app.wait()

libui.run(main())

Declarative API

The declarative API is the recommended way to build UIs. Describe your interface as a tree of components with reactive state — the framework handles synchronization.

State Management

State is a reactive container. When its value changes, all subscribers and bound widgets update automatically.

import libui
from libui.declarative import App, Window, VBox, Label, Button, State

async def main():
    app = App()

    name = State("World")
    count = State(0)

    # Derived state (read-only, auto-updates)
    greeting = name.map(lambda n: f"Hello, {n}!")

    # Subscribe to changes
    unsub = count.subscribe(lambda: print(count.value))

    def on_click():
        count.update(lambda n: n + 1)
        name.value = "Python"    # triggers greeting update

    app.build(window=Window("State Demo", 400, 300, child=VBox(
        Label(text=greeting),
        Label(text=count.map(lambda n: f"Clicks: {n}")),
        Button("Click", on_clicked=on_click),
    )))

    app.show()
    await app.wait()

libui.run(main())

Layout Containers

import libui
from libui.declarative import (
    App, Window, VBox, HBox, Group, Form, Tab, Grid, GridCell,
    Label, Button, Entry, MultilineEntry, stretchy,
)

async def main():
    app = App()

    app.build(window=Window("Layouts", 700, 500, child=Tab(
        # Vertical / Horizontal stacking
        ("Boxes", VBox(
            Label("Title"),
            Button("Click me"),
            stretchy(MultilineEntry()),  # stretchy = fills available space
            padded=True,
        )),

        # Form — two-column label + control layout
        ("Form", Form(
            ("Name:", Entry()),
            ("Password:", Entry(type="password")),
            ("Bio:", MultilineEntry(), True),  # True = stretchy
            padded=True,
        )),

        # Grid — precise positioning
        ("Grid", Grid(
            GridCell(Label("X:"), left=0, top=0, halign=libui.Align.END),
            GridCell(Entry(),      left=1, top=0, hexpand=True),
            GridCell(Label("Y:"), left=0, top=1, halign=libui.Align.END),
            GridCell(Entry(),      left=1, top=1, hexpand=True),
            GridCell(Button("OK"), left=0, top=2, xspan=2, halign=libui.Align.CENTER),
            padded=True,
        )),

        # Grouped container
        ("Group", Group("Connection", child=Form(
            ("Host:", Entry()),
            ("Port:", Entry()),
            padded=True,
        ), margined=True)),
    )))

    app.show()
    await app.wait()

libui.run(main())

Widgets

All widgets support reactive binding with State:

import libui
from libui.declarative import (
    App, Window, VBox, Form, Group, Separator, State, stretchy,
    Label, Button, Entry, MultilineEntry, Checkbox,
    Slider, Spinbox, ProgressBar,
    Combobox, RadioButtons, EditableCombobox,
    ColorButton, FontButton, DateTimePicker,
)

async def main():
    app = App()
    status = State("Ready.")
    value = State(50)
    dark_mode = State(False)

    app.build(window=Window("Widgets", 600, 500, child=VBox(
        Label(text=status),

        Group("Text Input", Form(
            ("Normal:", Entry(on_changed=lambda t: status.set(f"Entry: {t}"))),
            ("Password:", Entry(type="password")),
            ("Search:", Entry(type="search")),
            padded=True,
        )),

        Group("Controls", Form(
            ("Button:", Button("Save", on_clicked=lambda: status.set("Saved!"))),
            ("Checkbox:", Checkbox("Dark mode", checked=dark_mode,
                                   on_toggled=lambda c: status.set(f"Dark: {c}"))),
            ("Slider:", Slider(0, 100, value=value,
                               on_changed=lambda v: status.set(f"Value: {v}"))),
            ("Spinbox:", Spinbox(0, 999, value=value)),
            ("Progress:", ProgressBar(value=value)),
            padded=True,
        )),

        Group("Selection", Form(
            ("Combobox:", Combobox(items=["Red", "Green", "Blue"], selected=0)),
            ("Radio:", RadioButtons(items=["Low", "Medium", "High"])),
            ("Editable:", EditableCombobox(items=["Apple", "Banana"])),
            padded=True,
        )),

        Group("Pickers", Form(
            ("Color:", ColorButton(on_changed=lambda rgba: status.set(f"Color: {rgba}"))),
            ("Font:", FontButton(on_changed=lambda f: status.set(f"{f['family']} {f['size']}pt"))),
            ("DateTime:", DateTimePicker(type="datetime")),
            padded=True,
        )),

        Separator(),
        stretchy(MultilineEntry(wrapping=True)),
    )))

    app.show()
    await app.wait()

libui.run(main())

Data Tables

Tables use ListState — an observable list that automatically syncs insertions, deletions, and edits with the UI.

import libui
from libui.declarative import (
    App, Window, VBox, HBox, Label, Button, State, stretchy,
    DataTable, ListState,
    TextColumn, CheckboxTextColumn, ProgressColumn, ButtonColumn,
)

async def main():
    app = App()
    status = State("Click a row.")

    data = ListState([
        {"checked": 1, "name": "Alice", "role": "Engineer",  "score": 85, "action": "View"},
        {"checked": 0, "name": "Bob",   "role": "Designer",  "score": 72, "action": "View"},
        {"checked": 1, "name": "Carol", "role": "Manager",   "score": 90, "action": "View"},
    ])

    def on_button(row):
        d = data[row]
        app.msg_box("Details", f"{d['name']}{d['role']}\nScore: {d['score']}")

    table = DataTable(
        data,
        CheckboxTextColumn("Name", checkbox_key="checked", text_key="name",
                           checkbox_editable=True),
        TextColumn("Role", key="role"),
        ProgressColumn("Score", key="score"),
        ButtonColumn("Action", text_key="action", on_click=on_button),
        on_row_clicked=lambda row: status.set(f"Clicked: {data[row]['name']}"),
    )

    counter = State(len(data))

    def add_row():
        counter.update(lambda n: n + 1)
        data.append({"checked": 0, "name": f"Person {counter.value}",
                      "role": "New", "score": 50, "action": "View"})

    app.build(window=Window("Table", 600, 400, child=VBox(
        Label(text=status),
        stretchy(table),
        HBox(
            Button("Add Row", on_clicked=add_row),
            Button("Remove Last", on_clicked=lambda: data.pop() if len(data) else None),
        ),
    )))

    app.show()
    await app.wait()

libui.run(main())

Menus

Menus are defined before the window and passed to App.build():

import libui
from libui.declarative import (
    App, Window, VBox, Label, State,
    MenuDef, MenuItem, CheckMenuItem, MenuSeparator,
    QuitItem, PreferencesItem, AboutItem,
)

async def main():
    app = App()
    dark_state = State(False)
    status = State("Ready.")

    menus = [
        MenuDef("File",
            MenuItem("Open...", on_click=lambda: app.open_file()),
            MenuItem("Save As...", on_click=lambda: app.save_file()),
            MenuSeparator(),
            QuitItem(),
        ),
        MenuDef("Edit",
            CheckMenuItem("Dark Mode", checked=dark_state,
                          on_click=lambda: status.set(f"Dark: {dark_state.value}")),
            PreferencesItem(),
        ),
        MenuDef("Help",
            AboutItem(),
        ),
    ]

    app.build(
        menus=menus,
        window=Window("Menus", 500, 300, has_menubar=True, child=VBox(
            Label(text=status),
        )),
    )

    app.show()
    await app.wait()

libui.run(main())

Dialogs

import libui
from libui.declarative import App, Window, VBox, Button, Label, State

async def main():
    app = App()
    status = State("")

    def do_open():
        path = app.open_file()
        if path:
            status.set(f"Opened: {path}")

    async def do_open_async():
        path = await app.open_file_async()
        if path:
            status.set(f"Opened: {path}")

    app.build(window=Window("Dialogs", 400, 200, child=VBox(
        Label(text=status),
        Button("Open File (sync)", on_clicked=do_open),
        Button("Open File (async)", on_clicked=do_open_async),
        Button("Message Box", on_clicked=lambda: app.msg_box("Info", "Hello!")),
        Button("Error Box", on_clicked=lambda: app.msg_box_error("Error", "Something failed.")),
    )))

    app.show()
    await app.wait()

libui.run(main())

Custom Drawing

import math
import libui
from libui.declarative import App, Window, VBox, DrawArea, stretchy

def on_draw(ctx, area_w, area_h, clip_x, clip_y, clip_w, clip_h):
    # Filled rectangle
    path = libui.DrawPath()
    path.add_rectangle(20, 20, 200, 100)
    path.end()

    brush = libui.DrawBrush()
    brush.r, brush.g, brush.b, brush.a = 0.2, 0.6, 0.9, 1.0
    ctx.fill(path, brush)

    # Stroked circle
    circle = libui.DrawPath()
    circle.new_figure_with_arc(300, 70, 50, 0, 2 * math.pi, False)
    circle.end()

    stroke = libui.DrawStrokeParams()
    stroke.thickness = 3.0
    ctx.stroke(circle, brush, stroke)

    # Gradient
    grad_path = libui.DrawPath()
    grad_path.add_rectangle(20, 150, 200, 80)
    grad_path.end()

    grad = libui.DrawBrush()
    grad.type = libui.BrushType.LINEAR_GRADIENT
    grad.x0, grad.y0 = 20, 150
    grad.x1, grad.y1 = 220, 230
    grad.set_stops([(0.0, 1, 0, 0, 1), (0.5, 1, 1, 0, 1), (1.0, 0, 0, 1, 1)])
    ctx.fill(grad_path, grad)

    # Styled text
    text = libui.AttributedString("Hello Drawing!")
    text.set_attribute(libui.weight_attribute(libui.TextWeight.BOLD), 0, 5)
    text.set_attribute(libui.color_attribute(0.8, 0.0, 0.0, 1.0), 6, 14)
    layout = libui.DrawTextLayout(text, {"family": "sans-serif", "size": 18.0}, area_w)
    ctx.text(layout, 20, 260)

async def main():
    app = App()

    app.build(window=Window("Drawing", 500, 350,
        child=VBox(stretchy(DrawArea(on_draw=on_draw))),
    ))

    app.show()
    await app.wait()

libui.run(main())

The drawing API supports paths, fills, strokes, gradients (linear/radial), bezier curves, transforms (translate, rotate, scale, skew), clipping, and rich attributed text.

Full Declarative Example

import libui
from libui.declarative import *

async def main():
    app = App()
    status = State("Ready.")
    value = State(50)
    value.subscribe(lambda: status.set(f"Value: {value.value}"))

    app.build(
        menus=[
            MenuDef("File",
                MenuItem("About...", on_click=lambda: app.msg_box("About", "Demo v1.0")),
                MenuSeparator(),
                QuitItem(),
            ),
        ],
        window=Window("Demo", 600, 400, has_menubar=True, child=VBox(
            Label(text=status),
            Group("Controls", Form(
                ("Slider:", Slider(0, 100, value=value)),
                ("Spinbox:", Spinbox(0, 100, value=value)),
                ("Progress:", ProgressBar(value=value)),
                padded=True,
            )),
            Group("Selection", Form(
                ("Quality:", RadioButtons(items=["Low", "Medium", "High"])),
                ("Color:", Combobox(items=["Red", "Green", "Blue"], selected=0)),
                padded=True,
            )),
            Separator(),
            Group("Notes", stretchy(MultilineEntry(wrapping=True))),
        )),
    )

    app.show()
    await app.wait()

libui.run(main())

Imperative API

The imperative API gives you direct control over every widget. It maps closely to the underlying libui-ng C library and is useful when you need fine-grained control, custom event loops, or want to integrate with existing code.

All imperative widgets are thread-safe proxies — mutations are automatically marshalled to the UI thread.

Basic Example

import asyncio
import libui

async def main():
    window = libui.Window("Hello", 400, 300)

    box = libui.VerticalBox(padded=True)
    window.set_child(box)

    label = libui.Label("Count: 0")
    box.append(label)

    count = 0

    def on_click():
        nonlocal count
        count += 1
        label.text = f"Count: {count}"

    button = libui.Button("Click me!")
    button.on_clicked(on_click)
    box.append(button)

    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Containers

import asyncio
import libui

async def main():
    window = libui.Window("Containers", 600, 400)

    # Vertical / Horizontal box
    vbox = libui.VerticalBox(padded=True)
    hbox = libui.HorizontalBox(padded=True)
    vbox.append(hbox)
    hbox.append(libui.Button("Left"), stretchy=True)
    hbox.append(libui.Button("Right"), stretchy=True)

    # Group
    group = libui.Group("Settings")
    group.margined = True
    vbox.append(group)

    # Form — label + control pairs
    form = libui.Form(padded=True)
    form.append("Name:", libui.Entry())
    form.append("Bio:", libui.MultilineEntry(wrapping=True), stretchy=True)
    group.set_child(form)

    # Tab
    tab = libui.Tab()
    tab.append("Page 1", vbox)
    tab.append("Page 2", libui.Label("Second page"))
    tab.set_margined(0, True)
    tab.set_margined(1, True)

    # Grid
    grid = libui.Grid(padded=True)
    grid.append(libui.Label("Name:"), 0, 0, 1, 1, False, libui.Align.END, False, libui.Align.FILL)
    grid.append(libui.Entry(), 1, 0, 1, 1, True, libui.Align.FILL, False, libui.Align.FILL)

    window.set_child(tab)
    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Widgets

import asyncio
import libui

async def main():
    window = libui.Window("Widgets", 500, 600)
    vbox = libui.VerticalBox(padded=True)
    window.set_child(vbox)

    # Label
    label = libui.Label("Hello")
    vbox.append(label)

    # Button
    btn = libui.Button("Click")
    btn.on_clicked(lambda: setattr(label, "text", "Clicked!"))
    vbox.append(btn)

    # Entry (text input)
    entry = libui.Entry()                    # normal (also: "password", "search")
    entry.on_changed(lambda: setattr(label, "text", f"Entry: {entry.text}"))
    vbox.append(entry)

    # Multiline Entry
    mle = libui.MultilineEntry(wrapping=True)
    mle.text = "Type here..."
    vbox.append(mle, stretchy=True)

    # Checkbox
    cb = libui.Checkbox("Enable feature")
    cb.on_toggled(lambda: setattr(label, "text", f"Checked: {cb.checked}"))
    vbox.append(cb)

    # Slider
    slider = libui.Slider(0, 100)
    slider.on_changed(lambda: setattr(label, "text", f"Slider: {slider.value}"))
    vbox.append(slider)

    # Spinbox
    spinbox = libui.Spinbox(0, 100)
    spinbox.on_changed(lambda: setattr(label, "text", f"Spinbox: {spinbox.value}"))
    vbox.append(spinbox)

    # Progress Bar
    progress = libui.ProgressBar()
    progress.value = 75
    vbox.append(progress)

    # Combobox
    combo = libui.Combobox()
    for item in ("Red", "Green", "Blue"):
        combo.append(item)
    combo.selected = 0
    combo.on_selected(lambda: setattr(label, "text", f"Combo: {combo.selected}"))
    vbox.append(combo)

    # Radio Buttons
    radio = libui.RadioButtons()
    for item in ("Option A", "Option B"):
        radio.append(item)
    radio.on_selected(lambda: setattr(label, "text", f"Radio: {radio.selected}"))
    vbox.append(radio)

    # Separator
    vbox.append(libui.Separator())

    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Pickers

import asyncio
import libui

async def main():
    window = libui.Window("Pickers", 400, 300)
    form = libui.Form(padded=True)
    window.set_child(form)

    label = libui.Label("Pick something...")
    form.append("Status:", label)

    # Color picker
    color_btn = libui.ColorButton()
    color_btn.on_changed(lambda: setattr(
        label, "text", "Color: R={:.2f} G={:.2f} B={:.2f}".format(*color_btn.color[:3])))
    form.append("Color:", color_btn)

    # Font picker
    font_btn = libui.FontButton()
    font_btn.on_changed(lambda: setattr(
        label, "text", f"Font: {font_btn.font['family']} {font_btn.font['size']}pt"))
    form.append("Font:", font_btn)

    # Date/Time pickers
    dtp = libui.DateTimePicker()              # datetime
    dtp.on_changed(lambda: setattr(
        label, "text", "{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}".format(*dtp.time[:5])))
    form.append("DateTime:", dtp)

    dtp_date = libui.DateTimePicker(type="date")
    form.append("Date:", dtp_date)

    dtp_time = libui.DateTimePicker(type="time")
    form.append("Time:", dtp_time)

    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Tables

import asyncio
import libui

async def main():
    window = libui.Window("Table", 500, 300)

    data = [
        ["Alice", "Engineer", 85],
        ["Bob",   "Designer", 72],
        ["Carol", "Manager",  90],
    ]

    model = libui.TableModel(
        num_columns=lambda: 3,
        column_type=lambda col: libui.TableValueType.STRING if col < 2 else libui.TableValueType.INT,
        num_rows=lambda: len(data),
        cell_value=lambda row, col: str(data[row][col]) if col < 2 else int(data[row][col]),
        set_cell_value=lambda row, col, val: data[row].__setitem__(col, val),
    )

    table = libui.Table(model)
    table.append_text_column("Name", 0)
    table.append_text_column("Role", 1)
    table.append_progress_bar_column("Score", 2)

    table.on_row_clicked(lambda row: print(f"Clicked: {data[row][0]}"))

    # Dynamic updates
    data.append(["Dave", "Intern", 50])
    model.row_inserted(len(data) - 1)

    window.set_child(table)
    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Available column types:

table.append_text_column(name, col, editable=NEVER_EDITABLE)
table.append_checkbox_column(name, col, editable=ALWAYS_EDITABLE)
table.append_checkbox_text_column(name, cb_col, cb_editable, text_col)
table.append_progress_bar_column(name, col)
table.append_button_column(name, col, clickable=ALWAYS_EDITABLE)
table.append_image_column(name, col)
table.append_image_text_column(name, img_col, text_col)

Menus

Menus must be created before the window:

import asyncio
import libui

async def main():
    # Menus first
    file_menu = libui.Menu("File")
    item = file_menu.append_item("Open...")
    item.on_clicked(lambda: print("Open clicked"))
    file_menu.append_separator()
    file_menu.append_quit_item()

    edit_menu = libui.Menu("Edit")
    edit_menu.append_preferences_item()
    toggle = edit_menu.append_check_item("Dark Mode")
    toggle.on_clicked(lambda: print(f"Dark: {toggle.checked}"))

    help_menu = libui.Menu("Help")
    help_menu.append_about_item()

    # Then the window with has_menubar=True
    window = libui.Window("Menus", 500, 300, has_menubar=True)
    window.set_child(libui.Label("Menu demo"))
    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Drawing

import asyncio
import math
import libui

def on_draw(ctx, area_w, area_h, clip_x, clip_y, clip_w, clip_h):
    # Path + fill
    path = libui.DrawPath()
    path.add_rectangle(10, 10, 200, 100)
    path.end()

    brush = libui.DrawBrush()
    brush.r, brush.g, brush.b, brush.a = 0.8, 0.2, 0.2, 1.0
    ctx.fill(path, brush)

    # Stroke
    circle = libui.DrawPath()
    circle.new_figure_with_arc(160, 200, 50, 0, 2 * math.pi, False)
    circle.end()

    params = libui.DrawStrokeParams()
    params.thickness = 3.0
    params.cap = libui.LineCap.ROUND
    ctx.stroke(circle, brush, params)

    # Gradient
    rect = libui.DrawPath()
    rect.add_rectangle(250, 10, 150, 100)
    rect.end()

    grad = libui.DrawBrush()
    grad.type = libui.BrushType.LINEAR_GRADIENT
    grad.x0, grad.y0 = 250, 10
    grad.x1, grad.y1 = 400, 110
    grad.set_stops([(0.0, 1, 0, 0, 1), (1.0, 0, 0, 1, 1)])
    ctx.fill(rect, grad)

    # Transform
    matrix = libui.DrawMatrix()
    matrix.set_identity()
    matrix.rotate(300, 200, 30)
    ctx.save()
    ctx.transform(matrix)
    p = libui.DrawPath()
    p.add_rectangle(270, 185, 60, 30)
    p.end()
    ctx.fill(p, brush)
    ctx.restore()

    # Attributed text
    text = libui.AttributedString("Styled text")
    text.set_attribute(libui.weight_attribute(libui.TextWeight.BOLD), 0, 6)
    text.set_attribute(libui.color_attribute(0.0, 0.5, 0.0, 1.0), 7, 11)
    layout = libui.DrawTextLayout(text, {"family": "sans-serif", "size": 14.0}, area_w)
    ctx.text(layout, 10, 130)

async def main():
    window = libui.Window("Drawing", 500, 350)

    vbox = libui.VerticalBox()
    area = libui.Area(on_draw)
    vbox.append(area, stretchy=True)
    window.set_child(vbox)

    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Async Integration

libui.run() sets up a two-thread architecture: the main thread pumps the native event loop while an asyncio event loop runs on a background thread.

import asyncio
import libui

async def main():
    window = libui.Window("Async Demo", 500, 400)
    vbox = libui.VerticalBox(padded=True)
    window.set_child(vbox)

    label = libui.Label("Starting...")
    vbox.append(label)

    # Async callbacks work naturally
    async def on_click():
        label.text = "Fetching..."
        await asyncio.sleep(1)
        label.text = "Done!"

    btn = libui.Button("Go")
    btn.on_clicked(on_click)
    vbox.append(btn)

    # Background tasks
    async def ticker():
        n = 0
        while True:
            await asyncio.sleep(1)
            n += 1
            label.text = f"Tick #{n}"

    asyncio.create_task(ticker())

    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()
    await asyncio.Event().wait()

libui.run(main())

Dialogs

import asyncio
import libui

async def main():
    window = libui.Window("Dialogs", 400, 200)
    vbox = libui.VerticalBox(padded=True)
    window.set_child(vbox)

    label = libui.Label("")
    vbox.append(label)

    def do_open():
        path = libui.open_file(window)
        if path:
            label.text = f"Opened: {path}"

    btn_open = libui.Button("Open File")
    btn_open.on_clicked(do_open)

    btn_folder = libui.Button("Open Folder")
    btn_folder.on_clicked(lambda: setattr(
        label, "text", f"Folder: {libui.open_folder(window) or '(cancelled)'}"))

    btn_msg = libui.Button("Message Box")
    btn_msg.on_clicked(lambda: libui.msg_box(window, "Info", "Operation complete."))

    btn_err = libui.Button("Error Box")
    btn_err.on_clicked(lambda: libui.msg_box_error(window, "Error", "Something failed."))

    for b in (btn_open, btn_folder, btn_msg, btn_err):
        vbox.append(b)

    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()

    await asyncio.Event().wait()

libui.run(main())

Widget Reference

All Controls

Widget Description
Label Static or reactive text display
Button Clickable button
Entry Single-line text input (normal, password, search)
MultilineEntry Multi-line text editor
Checkbox Toggle with label
Slider Horizontal slider with range
Spinbox Numeric spinner with range
ProgressBar Progress indicator (0–100)
Combobox Dropdown selection
EditableCombobox Dropdown with text input
RadioButtons Mutually exclusive choices
ColorButton Color picker
FontButton Font picker
DateTimePicker Date, time, or datetime picker
Separator Visual divider (horizontal/vertical)
Area / DrawArea Custom 2D drawing surface
Table / DataTable Data grid with columns

Containers

Container Description
VBox / VerticalBox Stack children vertically
HBox / HorizontalBox Stack children horizontally
Group Titled container with border
Form Two-column label + control pairs
Tab Tabbed pages
Grid Position-based grid layout
Window Top-level window

Enumerations

Enum Values
libui.Align FILL, START, CENTER, END
libui.BrushType SOLID, LINEAR_GRADIENT, RADIAL_GRADIENT
libui.LineCap FLAT, ROUND, SQUARE
libui.LineJoin MITER, ROUND, BEVEL
libui.TextWeight THIN ... NORMAL ... BOLD ... MAXIMUM
libui.TextItalic NORMAL, OBLIQUE, ITALIC
libui.Underline NONE, SINGLE, DOUBLE, SUGGESTION
libui.SelectionMode NONE, ONE, ZERO_OR_ONE, MULTI
libui.SortIndicator NONE, ASCENDING, DESCENDING
libui.TableValueType INT, STRING, IMAGE
libui.TableModelColumn NEVER_EDITABLE, ALWAYS_EDITABLE

Examples

Run the examples from the project root:

python examples/hello.py                # minimal button + label
python examples/showcase_declarative.py # full declarative showcase (all widgets)
python examples/showcase.py             # full imperative showcase
python examples/example_async.py        # async widgets + background tasks
python examples/draw.py                 # custom drawing
python examples/table.py               # data table

Threading and the Main Thread

Why this matters

Native GUI toolkits have a fundamental rule: UI operations must happen on the main thread. On macOS (Cocoa) this is strictly enforced by the OS — violating it causes crashes and deadlocks. GTK+ and Win32 have similar constraints. Writing correct cross-platform GUI code means respecting this rule on every platform.

libui-python handles this for you. The library provides native asyncio integration with a completion queue that lets you write ordinary async Python while all UI work is transparently dispatched to the correct thread.

How it works

libui.run() starts a two-thread architecture:

  • Main thread — owns the native UI event loop and processes a completion queue
  • Background thread — runs your async def main() coroutine on a standard asyncio event loop

The high-level API (libui.Window, libui.Button, etc.) automatically dispatches every widget operation through core.queue_main() — a completion queue that executes functions on the main thread. You interact with widgets from async code as if threading didn't exist:

import asyncio
import libui

async def main():
    window = libui.Window("Hello", 400, 300)
    label = libui.Label("Ready")
    box = libui.VerticalBox(padded=True)
    box.append(label)
    window.set_child(box)

    async def update():
        await asyncio.sleep(1)
        label.text = "Updated!"  # dispatched to main thread automatically

    asyncio.create_task(update())
    window.on_closing(lambda: (libui.quit(), True)[-1])
    window.show()
    await asyncio.Event().wait()

libui.run(main())

Async callbacks work the same way — on_clicked, on_changed, and other event handlers accept both sync and async functions. Async handlers are scheduled on the asyncio loop automatically.

Safety guards

As an extra safety net, every low-level function in libui.core (the C extension) checks the calling thread at runtime. If you accidentally call a core function from the wrong thread, you get a clear RuntimeError instead of a crash:

RuntimeError: this function must be called from the main UI thread

This guard covers all operations: creating widgets, reading/writing properties, appending children, registering callbacks, showing dialogs, and drawing.

Using core directly

The high-level API handles threading transparently. If you use libui.core directly (for tests, custom event loops, or advanced use cases), these rules apply:

Function Thread-safe? Notes
core.queue_main(fn) Yes Enqueues fn onto the main-thread completion queue
core.is_main_thread() Yes Returns True if on the UI thread
core.quit() Yes Can be called from any thread
Everything else in core No Must be on the main thread — raises RuntimeError otherwise

For explicit cross-thread dispatch, use the bridge helpers:

from libui.loop import invoke_on_main, invoke_on_main_async

# From a sync background thread (blocks until complete):
result = invoke_on_main(some_core_function, arg1, arg2)

# From an async coroutine (non-blocking):
result = await invoke_on_main_async(some_core_function, arg1, arg2)

Both schedule the function on the main thread via the completion queue and return the result (or re-raise any exception).

Requirements

  • Python >= 3.13
  • Linux: GTK+-3.0
  • macOS: Cocoa (built-in)
  • Windows: Win32 API (built-in)

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

libui-0.2.0.tar.gz (927.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

libui-0.2.0-cp314-cp314-win_amd64.whl (191.9 kB view details)

Uploaded CPython 3.14Windows x86-64

libui-0.2.0-cp314-cp314-manylinux_2_28_x86_64.whl (12.3 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.28+ x86-64

libui-0.2.0-cp314-cp314-macosx_10_15_universal2.whl (373.3 kB view details)

Uploaded CPython 3.14macOS 10.15+ universal2 (ARM64, x86-64)

libui-0.2.0-cp313-cp313-win_amd64.whl (186.9 kB view details)

Uploaded CPython 3.13Windows x86-64

libui-0.2.0-cp313-cp313-manylinux_2_28_x86_64.whl (12.3 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.28+ x86-64

libui-0.2.0-cp313-cp313-macosx_10_13_universal2.whl (373.2 kB view details)

Uploaded CPython 3.13macOS 10.13+ universal2 (ARM64, x86-64)

libui-0.2.0-cp312-cp312-win_amd64.whl (186.9 kB view details)

Uploaded CPython 3.12Windows x86-64

libui-0.2.0-cp312-cp312-manylinux_2_28_x86_64.whl (12.3 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.28+ x86-64

libui-0.2.0-cp312-cp312-macosx_10_13_universal2.whl (373.2 kB view details)

Uploaded CPython 3.12macOS 10.13+ universal2 (ARM64, x86-64)

libui-0.2.0-cp311-cp311-win_amd64.whl (186.9 kB view details)

Uploaded CPython 3.11Windows x86-64

libui-0.2.0-cp311-cp311-manylinux_2_28_x86_64.whl (12.3 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.28+ x86-64

libui-0.2.0-cp311-cp311-macosx_10_9_universal2.whl (372.8 kB view details)

Uploaded CPython 3.11macOS 10.9+ universal2 (ARM64, x86-64)

libui-0.2.0-cp310-cp310-win_amd64.whl (186.9 kB view details)

Uploaded CPython 3.10Windows x86-64

libui-0.2.0-cp310-cp310-manylinux_2_28_x86_64.whl (12.3 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.28+ x86-64

libui-0.2.0-cp310-cp310-macosx_10_9_universal2.whl (372.8 kB view details)

Uploaded CPython 3.10macOS 10.9+ universal2 (ARM64, x86-64)

File details

Details for the file libui-0.2.0.tar.gz.

File metadata

  • Download URL: libui-0.2.0.tar.gz
  • Upload date:
  • Size: 927.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for libui-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6e125d001f7b0a9d9c67a5969658ebf8af4e4d96405d183cc404dff58ef94ee0
MD5 57554923e983d6653d79177bead53e99
BLAKE2b-256 1a5f35416d025ba8bf0c8f0b414678d26ce2e2a79bd0762a893788702dfa1ba3

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0.tar.gz:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp314-cp314-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.0-cp314-cp314-win_amd64.whl
  • Upload date:
  • Size: 191.9 kB
  • Tags: CPython 3.14, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for libui-0.2.0-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 9c59a3048f35a21618d490f159b930307f271aeafa6b807980754fb23de4e3fd
MD5 8ba0e25281a2cfc395c478b502c94c6d
BLAKE2b-256 d3aa932495a70887adfebf9027cb8a6e094ea0f14ca7cd0a44cce99464c4db88

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp314-cp314-win_amd64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp314-cp314-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 0018a7ac6c14869b6ae705842e98715be797a55caac15713a97ff1737c4ed856
MD5 ff87b5370642e7384dc5456a184e5314
BLAKE2b-256 f4cd124fa8026532630189d5dc93a4f429bf7b7edd71cb3f1ce7b0a5d2040b1a

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp314-cp314-manylinux_2_28_x86_64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp314-cp314-macosx_10_15_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp314-cp314-macosx_10_15_universal2.whl
Algorithm Hash digest
SHA256 a5d432d41365d8c19d3bd9f708c092e9ed4ca4cca9288dc47a81870aef31654d
MD5 0e575e002d3a8e86434bdb5b2a1fc16e
BLAKE2b-256 2949ffcfcf6e15d49b6babe8d92cf9cce8df23acf3f3e497258c1c95ab5fcfc2

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp314-cp314-macosx_10_15_universal2.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp313-cp313-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.0-cp313-cp313-win_amd64.whl
  • Upload date:
  • Size: 186.9 kB
  • Tags: CPython 3.13, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for libui-0.2.0-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 ca9662e2be41c7f02532c8853acbe2e9b4536f6aaee925dad8c7100f4731d5a0
MD5 83e879c19cf94e7e87e1f956d9700fd5
BLAKE2b-256 b257c4ee59d08fc0796dcd356a88056b0b6573841e684533380b8fb8b063f9b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp313-cp313-win_amd64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp313-cp313-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp313-cp313-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 c8de6042db7ed93c49556b4bb6addb266461808967f0452c79a5d82acc5139d1
MD5 f3767e1d31cd806bb43de4d589eb3877
BLAKE2b-256 dd6cf61c51f410de178a7a8392b39bb394d570c66f4fdd2b81b0b6a7033489ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp313-cp313-manylinux_2_28_x86_64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp313-cp313-macosx_10_13_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp313-cp313-macosx_10_13_universal2.whl
Algorithm Hash digest
SHA256 de28eb2a8da73fc2ce04fbd3066bfa9c597c27f09006bc38c9e6416bce39689f
MD5 087c56597d4862a419a7b8223ba571bc
BLAKE2b-256 1a22cd70086f4c7904cdd86005c0d16f0b3e11f6a085077aae8b72dd309f0413

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp313-cp313-macosx_10_13_universal2.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.0-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 186.9 kB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for libui-0.2.0-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 dd23da9836269f358fd89f56b6baf33e0c2e7f113966683da4f34b35c80507f1
MD5 94bda157216b8253cb21d72e0f630c68
BLAKE2b-256 d4f30055ab1cda5cbc55ff5efbe548467fa18cb7d306acbe68fd842fbc490abb

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp312-cp312-win_amd64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp312-cp312-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp312-cp312-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 a7cd90a47900328ce236a503566fee0df929b5f94783ad6103010cf790bb4935
MD5 d18c18d6b0b0066f0641c8d3b85afc31
BLAKE2b-256 b015f365661df02b087bfb14b13d04e77194f7bdbbf54a51aeb8fa886a325c62

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp312-cp312-manylinux_2_28_x86_64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp312-cp312-macosx_10_13_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp312-cp312-macosx_10_13_universal2.whl
Algorithm Hash digest
SHA256 126d6a9dd59e3ab05634ce82f7fb1a908faa0acf1c13fbaba7f5da9a29eae995
MD5 ac05a3c52c20e29b5072ecc54bd206f2
BLAKE2b-256 7d7803ed4a0ba96731c2007de5b352fd48ccba27b64ca699ae99ecf30ff622fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp312-cp312-macosx_10_13_universal2.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.0-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 186.9 kB
  • Tags: CPython 3.11, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for libui-0.2.0-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 35532be648e514c65edd8a1bd76d9d1cba941f0e672c78d9bf141a904ea7ceb9
MD5 4a8e9f7f6a23d9917afc331fa37db901
BLAKE2b-256 3bfad88e939adbb91928bfabc36cb4c265982b7305032069c46faf919979e78d

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp311-cp311-win_amd64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp311-cp311-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp311-cp311-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 97daac4ff91cb9364455baccdae134be17c2bb2a699ae6b6eadb53ddf8ea2cc3
MD5 c547c418351bc800a3b090811c0dacc3
BLAKE2b-256 cdd60f06b970d49a686ca9c2339ebebe351c0ed18bb7f2bbab1fadf0fa20f2ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp311-cp311-manylinux_2_28_x86_64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp311-cp311-macosx_10_9_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp311-cp311-macosx_10_9_universal2.whl
Algorithm Hash digest
SHA256 2c695479a55cd0a4792af7b4237d02a4016e28bedb119c6de4db675263786b05
MD5 63b44a556cc347cdadae604b549837ea
BLAKE2b-256 8f1ce6796e8205c562d9290e16b5fda8ac7f9566d4fe12be49695163d25ba81d

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp311-cp311-macosx_10_9_universal2.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp310-cp310-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.0-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 186.9 kB
  • Tags: CPython 3.10, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for libui-0.2.0-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 66b15e3178be6920eae9fa9d5f9559b7bd715737cfff5831662a97632932c2c8
MD5 cf39c3e5934c5c5f7a61f9c98a26b4ed
BLAKE2b-256 a91b6ea0cc94c2451f95fa8757d882e03c4585854e9a40f26a7f738fc6863191

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp310-cp310-win_amd64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp310-cp310-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp310-cp310-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 e15e670285bacbc5711a407d658782c22d8c4c4b5804c17f64c3f2a628665074
MD5 59ec38c6b5c9773e7f03d9380445ccd3
BLAKE2b-256 2906d05d3f5f6656935dc5e68f56ffd0de525a40ad21947f4fdbdb7b30d78687

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp310-cp310-manylinux_2_28_x86_64.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libui-0.2.0-cp310-cp310-macosx_10_9_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.0-cp310-cp310-macosx_10_9_universal2.whl
Algorithm Hash digest
SHA256 7a74c74d3acd99c43ef534d4a58d2197d124f1aa743c8153af0f0be4ab198875
MD5 7e39e012ff60c2ea5c6acac9ee228388
BLAKE2b-256 d8e5145750031c2bfbeedf2e166fe7b7e13e1242b6a219f0b69f47746b8637fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.0-cp310-cp310-macosx_10_9_universal2.whl:

Publisher: release.yml on mosquito/libui-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page