Skip to main content

Python bindings for libui-ng

Project description

libui-python

libui-python

CI PyPI Python License Dependencies Platform Native UI

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.2.tar.gz (928.9 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.2-cp314-cp314-win_amd64.whl (192.3 kB view details)

Uploaded CPython 3.14Windows x86-64

libui-0.2.2-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.2-cp314-cp314-macosx_10_15_universal2.whl (373.5 kB view details)

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

libui-0.2.2-cp313-cp313-win_amd64.whl (187.2 kB view details)

Uploaded CPython 3.13Windows x86-64

libui-0.2.2-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.2-cp313-cp313-macosx_10_13_universal2.whl (373.4 kB view details)

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

libui-0.2.2-cp312-cp312-win_amd64.whl (187.2 kB view details)

Uploaded CPython 3.12Windows x86-64

libui-0.2.2-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.2-cp312-cp312-macosx_10_13_universal2.whl (373.4 kB view details)

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

libui-0.2.2-cp311-cp311-win_amd64.whl (187.2 kB view details)

Uploaded CPython 3.11Windows x86-64

libui-0.2.2-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.2-cp311-cp311-macosx_10_9_universal2.whl (373.0 kB view details)

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

libui-0.2.2-cp310-cp310-win_amd64.whl (187.2 kB view details)

Uploaded CPython 3.10Windows x86-64

libui-0.2.2-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.2-cp310-cp310-macosx_10_9_universal2.whl (373.0 kB view details)

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

File details

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

File metadata

  • Download URL: libui-0.2.2.tar.gz
  • Upload date:
  • Size: 928.9 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.2.tar.gz
Algorithm Hash digest
SHA256 f0b552d6626ff829425ce878669da3ec6cef7d79616187d45df01f3e5e041a62
MD5 1fdb23bfd8020e889af4508638066c29
BLAKE2b-256 d283c1610a09e53a0f9f5d1d164bc16f9269cf7e0460c9084cd404622f5f6e09

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2.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.2-cp314-cp314-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.2-cp314-cp314-win_amd64.whl
  • Upload date:
  • Size: 192.3 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.2-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 8de09dd7b78600579c9ed2c9d82857c704534788da9f9b3f319e1eafc1ac97d9
MD5 e119620f987d9af98ca3632c53876e46
BLAKE2b-256 8ec0ed8b25899037180c620c9bfd56fbe9e7f93572a9c91f04da44deb9b204d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp314-cp314-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 d30aa32c956135f11f28e575a1aa3fbc912fedaae7c905d9f51191f58b8f1dd5
MD5 1502de31aa30885c701caae49d1e12ed
BLAKE2b-256 f1de88f06d4f609eccaa803598cc926870a2ed6daa5ea71ea799cd1c8fe34f70

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp314-cp314-macosx_10_15_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp314-cp314-macosx_10_15_universal2.whl
Algorithm Hash digest
SHA256 40f62b70a859e8e0c218da0ab461a0c0b8a707c86967bbb6635d7c8b0c6ee1a6
MD5 9ca70b55052a16c91bebf55302c7aa8f
BLAKE2b-256 695ca9fee941eccdb2dba12ff754ff3eab7442f7d46ace1310969690c11681ed

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp313-cp313-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.2-cp313-cp313-win_amd64.whl
  • Upload date:
  • Size: 187.2 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.2-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 37658721c7ec638e37c1b70b6b6aaff26b4f41985c40607e9420de13114e3e10
MD5 903d186a4714b2348e281c200504a09a
BLAKE2b-256 a85e9d0bc891d290563d7296924ec970f554d981df010d3090578fe0cac909c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp313-cp313-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp313-cp313-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 f7dfdaff8e83cf4e654884ce3c5d07a5b2cc9cee62492abb2acbee6e5ac8b8c0
MD5 dbd4cb33099c99a63ffc7abf17dbbcd6
BLAKE2b-256 ed3a2363d3943811798f1affd55b7080abfa1f39814fdbbf4fa5ca7d9860e548

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp313-cp313-macosx_10_13_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp313-cp313-macosx_10_13_universal2.whl
Algorithm Hash digest
SHA256 307dddc53bfaaf09d1a77842570558b95bc5d37126a59cf693c4defd40e197df
MD5 4ae0f8e52576dc953fa7653f613a2479
BLAKE2b-256 7455deb0128e945a13e351253980ea857552c9fccc6663710a5fa116cc26356e

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.2-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 187.2 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.2-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 1cc57594c7e1f0fb32b9f08f5b5f729531397b3359da3012068fd4afd424f51a
MD5 dd653315d075e7d42b20df22df42b354
BLAKE2b-256 79187447fa0ded9abbc68587a098ed70b274376c21c4410c202f6445d287a6a4

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp312-cp312-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp312-cp312-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 26fefc08313a92c1751a39225da0a293367e13c06c34ea23c8a94446d6bacd00
MD5 b520c8a8c86ad0e7da6f5b860500eb10
BLAKE2b-256 26e4e0148a362383977285a2880912d8c3996af61e7ffb9b7f10ae9d80eaa2c6

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp312-cp312-macosx_10_13_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp312-cp312-macosx_10_13_universal2.whl
Algorithm Hash digest
SHA256 271a354b1cae200446a5c59a6f570471bcab90776f58f061a1101bb09ad3d7a3
MD5 f0543ed0064ebc724a833d325519fdaa
BLAKE2b-256 c86b7b233e4496e2996bfa7ffa29fe034309fb68b2a243efeb4c933836236367

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.2-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 187.2 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.2-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 84bfe205a23eba152417e89c96a17416a221bc9c69f6764c0ba6eeb4cc21d61d
MD5 b4a222f3f26b5f914b5828907919fce5
BLAKE2b-256 126c183f74227ee69d463d0825869ba21d33e3a27a83036f15a423a2318254d6

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp311-cp311-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp311-cp311-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 e4bad91c2f4344c66fb83f311baf793295e223a2894336c3aa4b831ef7178798
MD5 18d469d02a82962770aefb8c5470077d
BLAKE2b-256 a08235a3d2c8fa59078bab908e468ab09066950dc63c3a7be791a23d2dab7626

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp311-cp311-macosx_10_9_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp311-cp311-macosx_10_9_universal2.whl
Algorithm Hash digest
SHA256 aa7b1192d5abe87f04eccbd7da05ea223e639c29c1915991f1aceea766e28f75
MD5 dfea70ff9e9d610fb7a51c8ba8c91520
BLAKE2b-256 c6dfeb535dd261693edadb03778921fa0b51440e196766abf3bddc81d354349c

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp310-cp310-win_amd64.whl.

File metadata

  • Download URL: libui-0.2.2-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 187.2 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.2-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 32cbe3cf2530de97a7b22b9de0f52f865682dc064728edfbf2beb51dd37f5a1d
MD5 2aef812c3833b8623d91b0902bb933b3
BLAKE2b-256 62eb8c10d7d121df3a73932c90b543f3258b291b6c016bfa4f9a140383a676c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp310-cp310-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp310-cp310-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 1740be73353a897dd40f04e005a72fad3d1e2d36cd09ee90d3e72db4aa808425
MD5 bcd22faff309c7bc9d9bc6ec00bba4af
BLAKE2b-256 ab3dddeac8b84d20fe08b084ba84eeda6bcbddad9142695740e0c47cc4695c20

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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.2-cp310-cp310-macosx_10_9_universal2.whl.

File metadata

File hashes

Hashes for libui-0.2.2-cp310-cp310-macosx_10_9_universal2.whl
Algorithm Hash digest
SHA256 5312304415c48743e063ea7217963843c418711b66d56cb5d89ca257ffab6593
MD5 751cb4da60dc3c5ef715a8e389bdf92c
BLAKE2b-256 fc824656bbd37651e0448247ebf6a3e6e957873048b38d3ade2639b24f614d45

See more details on using hashes here.

Provenance

The following attestation bundles were made for libui-0.2.2-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