Async¶
libui-python has first-class asyncio support. Your application runs as an async function, and event callbacks can be either sync or async.
Background tasks¶
Use asyncio.create_task() to run background work while the UI stays responsive:
"""Async background tasks — concurrent work with UI updates."""
import asyncio
import libui
from libui.declarative import (
App,
Window,
VBox,
Label,
Button,
ProgressBar,
State,
)
async def main():
app = App()
status = State("Ready.")
progress = State(0)
async def do_work():
status.set("Working...")
for i in range(1, 101):
await asyncio.sleep(0.03)
progress.set(i)
status.set("Done!")
await asyncio.sleep(1)
progress.set(0)
status.set("Ready.")
# Background ticker
async def ticker():
n = 0
while True:
await asyncio.sleep(2)
n += 1
if progress.value == 0:
status.set(f"Idle tick #{n}")
asyncio.create_task(ticker())
app.build(
window=Window(
"Async Background",
400,
200,
child=VBox(
Label(text=status),
ProgressBar(value=progress),
Button("Start Work", on_clicked=do_work),
),
)
)
app.show()
await app.wait()
libui.run(main())
The proxy layer handles thread safety — you can set widget properties from async code without worrying about which thread you’re on.
Async callbacks¶
Event handlers can be async def functions. The framework automatically schedules them on the asyncio loop:
"""Async callbacks — event handlers that are coroutines."""
import asyncio
import libui
from libui.declarative import (
App,
Window,
VBox,
Label,
Button,
ProgressBar,
State,
)
async def main():
app = App()
status = State("Click a button.")
progress = State(0)
async def fetch_data():
"""Simulate an async network request."""
status.set("Fetching data...")
for i in range(1, 101):
await asyncio.sleep(0.02)
progress.set(i)
status.set("Data loaded!")
progress.set(0)
async def save_data():
"""Simulate saving with a dialog."""
path = await app.save_file_async()
if not path:
status.set("Save cancelled.")
return
status.set(f"Saving to {path}...")
await asyncio.sleep(1)
status.set(f"Saved to {path}")
def sync_action():
"""A sync callback runs on the main thread."""
status.set("Sync action executed!")
app.build(
window=Window(
"Async Callbacks",
450,
250,
child=VBox(
Label(text=status),
ProgressBar(value=progress),
Button("Fetch Data (async)", on_clicked=fetch_data),
Button("Save Data (async)", on_clicked=save_data),
Button("Sync Action", on_clicked=sync_action),
),
)
)
app.show()
await app.wait()
libui.run(main())
Key points:
Sync callbacks run immediately on the main thread
Async callbacks are scheduled on the asyncio event loop (background thread)
Both types can safely update widget properties through the proxy layer
Use
await app.msg_box_async(...)instead ofapp.msg_box(...)from async callbacks
How it works¶
libui.run(coro) creates a two-thread architecture:
Main thread Background thread
| |
|-- init libui |
|-- pump main_step() <--> |-- asyncio.run(coro)
| ^ | |
| |-- queue_main(fn) ------|-----|
| |
|-- cleanup |-- done
Main thread: owns the native UI event loop
Background thread: runs your
async def main()coroutinequeue_main(fn): enqueues a function to run on the main threadProxy layer: automatically dispatches property access through
queue_main
Cross-thread helpers¶
For advanced use cases with libui.core directly:
from libui.loop import invoke_on_main, invoke_on_main_async
# Blocking (from sync code on any thread):
result = invoke_on_main(core_function, arg1, arg2)
# Non-blocking (from async code):
result = await invoke_on_main_async(core_function, arg1, arg2)