Layouts¶
libui-python provides several containers for arranging widgets. This chapter covers all of them.
VBox and HBox¶
VBox stacks children vertically, HBox horizontally. Use stretchy() to make a child fill available space:
"""VBox and HBox — vertical and horizontal stacking with stretchy."""
import libui
from libui.declarative import (
App,
Window,
VBox,
HBox,
Label,
Button,
MultilineEntry,
Separator,
stretchy,
)
async def main():
app = App()
app.build(
window=Window(
"VBox & HBox",
500,
400,
child=VBox(
Label("Top (fixed)"),
Separator(),
HBox(
stretchy(Button("Left (stretchy)")),
Button("Center (fixed)"),
stretchy(Button("Right (stretchy)")),
),
Separator(),
stretchy(
MultilineEntry(
text="This multiline entry is stretchy — it fills the remaining space.",
wrapping=True,
)
),
Label("Bottom (fixed)"),
),
)
)
app.show()
await app.wait()
libui.run(main())
Form¶
Form creates a two-column layout with labels on the left and controls on the right:
"""Form layout — two-column label + control pairs."""
import libui
from libui.declarative import (
App,
Window,
VBox,
Form,
Group,
Label,
Entry,
MultilineEntry,
Combobox,
State,
stretchy,
)
async def main():
app = App()
status = State("Fill in the form below.")
app.build(
window=Window(
"Form Layout",
500,
400,
child=VBox(
Label(text=status),
stretchy(
Group(
"User Profile",
Form(
(
"First name:",
Entry(
on_changed=lambda t: status.set(f"First: {t}"),
),
),
(
"Last name:",
Entry(
on_changed=lambda t: status.set(f"Last: {t}"),
),
),
("Email:", Entry()),
(
"Role:",
Combobox(
items=["Admin", "Editor", "Viewer"],
selected=0,
),
),
(
"Bio:",
MultilineEntry(wrapping=True),
True,
), # True = stretchy
padded=True,
),
)
),
),
)
)
app.show()
await app.wait()
libui.run(main())
Each row is a tuple: (label, widget) or (label, widget, stretchy).
Tab¶
Tab creates a tabbed container. Each page is a (name, widget) tuple:
"""Tab container — multiple pages in a tabbed view."""
import libui
from libui.declarative import (
App,
Window,
Tab,
VBox,
Form,
Label,
Button,
Entry,
MultilineEntry,
stretchy,
)
async def main():
app = App()
app.build(
window=Window(
"Tabs",
500,
350,
child=Tab(
(
"Profile",
Form(
("Name:", Entry()),
("Email:", Entry()),
padded=True,
),
),
(
"Settings",
VBox(
Label("Application settings go here."),
Button("Reset to Defaults"),
),
),
(
"Notes",
VBox(
stretchy(
MultilineEntry(
text="Write your notes here...",
wrapping=True,
)
),
),
),
),
)
)
app.show()
await app.wait()
libui.run(main())
Grid¶
Grid places children at exact (column, row) coordinates with optional spanning — useful for keypad-style layouts and other 2D arrangements that VBox/HBox/Form cannot express:
"""Grid layout — position-based coordinate placement with GridCell.
Grid places widgets at exact (column, row) coordinates with spanning.
It is best for keypad-style layouts where you need precise 2D placement
that VBox/HBox/Form cannot express.
Note: on macOS, Grid does not expand to fill its parent width —
use Form for label + control layouts that need to stretch.
"""
import libui
from libui.declarative import App, Window, VBox, Grid, GridCell, Button, Entry, State
async def main():
app = App()
display = State("0")
def press(ch):
cur = display.value
if cur == "0" and ch not in (".", "0"):
display.set(ch)
else:
display.set(cur + ch)
def clear():
display.set("0")
def btn(text, left, top, xspan=1):
return GridCell(
Button(text, on_clicked=lambda t=text: press(t)),
left,
top,
xspan=xspan,
)
keypad = Grid(
# Row 0: display spanning 4 columns
GridCell(Entry(text=display, read_only=True), 0, 0, xspan=4),
# Row 1
btn("7", 0, 1),
btn("8", 1, 1),
btn("9", 2, 1),
GridCell(Button("C", on_clicked=clear), 3, 1),
# Row 2
btn("4", 0, 2),
btn("5", 1, 2),
btn("6", 2, 2),
btn("+", 3, 2),
# Row 3
btn("1", 0, 3),
btn("2", 1, 3),
btn("3", 2, 3),
btn("-", 3, 3),
# Row 4: zero spans 2 columns
btn("0", 0, 4, xspan=2),
btn(".", 2, 4),
btn("=", 3, 4),
padded=True,
)
app.build(
window=Window(
"Grid Layout — Calculator",
300,
250,
child=VBox(keypad),
)
)
app.show()
await app.wait()
libui.run(main())
Key GridCell parameters:
Parameter |
Description |
|---|---|
|
Grid position (zero-based) |
|
How many columns/rows to span |
|
Whether to expand to fill space |
|
Alignment within the cell ( |
Note
On macOS, Grid cells do not expand horizontally even with hexpand=True — this is a known libui-ng upstream bug in the Cocoa backend’s Auto Layout constraints. vexpand works correctly. For label + control forms that need to stretch horizontally, use Form instead. Grid works well for fixed-size coordinate layouts like keypads and toolbars.
Group¶
Group wraps a single child with a titled border. Combine with other containers for labeled sections:
Group("Connection Settings", child=Form(
("Host:", Entry()),
("Port:", Entry()),
), margined=True)
stretchy¶
stretchy(node) marks a child as expandable in VBox or HBox. Without it, children take only their natural size. Only children marked stretchy will grow to fill available space.
VBox(
Label("Fixed size"),
stretchy(MultilineEntry()), # fills remaining space
Button("Also fixed"),
)