Drawing

DrawArea provides a 2D drawing surface with paths, fills, strokes, gradients, transforms, and text.

Basic shapes

The on_draw callback receives a drawing context and the area dimensions:

"""Drawing shapes — rectangles, circles, triangles, and strokes."""

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()

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

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

    red = libui.DrawBrush()
    red.r, red.g, red.b, red.a = 0.8, 0.2, 0.2, 1.0

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

    # Filled triangle
    tri = libui.DrawPath()
    tri.new_figure(20, 200)
    tri.line_to(120, 140)
    tri.line_to(220, 200)
    tri.close_figure()
    tri.end()

    green = libui.DrawBrush()
    green.r, green.g, green.b, green.a = 0.2, 0.7, 0.3, 1.0
    ctx.fill(tri, green)

    # Bezier curve
    bezier = libui.DrawPath()
    bezier.new_figure(250, 200)
    bezier.bezier_to(300, 120, 400, 220, 450, 150)
    bezier.end()

    purple = libui.DrawBrush()
    purple.r, purple.g, purple.b, purple.a = 0.5, 0.0, 0.7, 1.0

    sp = libui.DrawStrokeParams()
    sp.thickness = 2.5
    sp.cap = libui.LineCap.ROUND
    ctx.stroke(bezier, purple, sp)


async def main():
    app = App()

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

    app.show()
    await app.wait()


libui.run(main())
Drawing shapes

The drawing workflow is:

  1. Create a DrawPath and add geometry (rectangles, arcs, lines)

  2. Call path.end() to close the path

  3. Create a DrawBrush with color

  4. Call ctx.fill(path, brush) or ctx.stroke(path, brush, stroke_params)

Path methods

Method

Description

add_rectangle(x, y, w, h)

Add a rectangle

new_figure(x, y)

Start a new sub-path at a point

new_figure_with_arc(cx, cy, r, start, sweep, neg)

Start with an arc

line_to(x, y)

Draw a line to a point

bezier_to(c1x, c1y, c2x, c2y, ex, ey)

Cubic bezier curve

close_figure()

Close the current sub-path

end()

Finalize the path (required before use)

Stroke parameters

DrawStrokeParams controls line appearance:

sp = libui.DrawStrokeParams()
sp.thickness = 3.0
sp.cap = libui.LineCap.ROUND    # FLAT, ROUND, SQUARE
sp.join = libui.LineJoin.ROUND  # MITER, ROUND, BEVEL
sp.set_dashes([10.0, 5.0])     # dash pattern

Gradients

Both linear and radial gradients are supported:

"""Drawing gradients — linear and radial gradient fills."""

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):
    # Linear gradient
    rect = libui.DrawPath()
    rect.add_rectangle(20, 20, 200, 100)
    rect.end()

    lin = libui.DrawBrush()
    lin.type = libui.BrushType.LINEAR_GRADIENT
    lin.x0, lin.y0 = 20, 20
    lin.x1, lin.y1 = 220, 120
    lin.set_stops(
        [
            (0.0, 1.0, 0.0, 0.0, 1.0),  # red
            (0.5, 1.0, 1.0, 0.0, 1.0),  # yellow
            (1.0, 0.0, 0.0, 1.0, 1.0),  # blue
        ]
    )
    ctx.fill(rect, lin)

    # Radial gradient
    circle = libui.DrawPath()
    circle.new_figure_with_arc(370, 70, 60, 0, 2 * math.pi, False)
    circle.end()

    rad = libui.DrawBrush()
    rad.type = libui.BrushType.RADIAL_GRADIENT
    rad.x0, rad.y0 = 370, 70  # center
    rad.x1, rad.y1 = 370, 70  # focus (same as center)
    rad.outer_radius = 60
    rad.set_stops(
        [
            (0.0, 1.0, 1.0, 1.0, 1.0),  # white center
            (1.0, 0.2, 0.0, 0.6, 1.0),  # purple edge
        ]
    )
    ctx.fill(circle, rad)

    # Another linear gradient — vertical
    rect2 = libui.DrawPath()
    rect2.add_rectangle(20, 150, 430, 60)
    rect2.end()

    lin2 = libui.DrawBrush()
    lin2.type = libui.BrushType.LINEAR_GRADIENT
    lin2.x0, lin2.y0 = 20, 150
    lin2.x1, lin2.y1 = 450, 150
    lin2.set_stops(
        [
            (0.0, 0.0, 0.8, 0.0, 1.0),  # green
            (0.5, 0.0, 0.8, 0.8, 1.0),  # teal
            (1.0, 0.0, 0.0, 0.8, 1.0),  # blue
        ]
    )
    ctx.fill(rect2, lin2)


async def main():
    app = App()

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

    app.show()
    await app.wait()


libui.run(main())
Gradients

Gradient stops are tuples of (position, r, g, b, a) where position is 0.0 to 1.0.

Styled text

AttributedString supports rich text with attributes for weight, color, style, and more:

"""Drawing styled text — attributed strings with formatting."""

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):
    # Create an attributed string with various styles
    text = "Bold Colored Italic Underlined"
    astr = libui.AttributedString(text)

    # Bold (0-4)
    astr.set_attribute(libui.weight_attribute(libui.TextWeight.BOLD), 0, 4)

    # Red color (5-12)
    astr.set_attribute(libui.color_attribute(0.8, 0.0, 0.0, 1.0), 5, 12)

    # Italic (13-19)
    astr.set_attribute(libui.italic_attribute(libui.TextItalic.ITALIC), 13, 19)

    # Underline (20-30)
    astr.set_attribute(libui.underline_attribute(libui.Underline.SINGLE), 20, 30)

    font = {"family": "sans-serif", "size": 18.0}
    layout = libui.DrawTextLayout(astr, font, area_w - 40)
    ctx.text(layout, 20, 20)

    # Second line with more attributes
    text2 = "Large serif text with background highlight"
    astr2 = libui.AttributedString(text2)
    astr2.set_attribute(libui.family_attribute("serif"), 0, len(text2))
    astr2.set_attribute(libui.size_attribute(24.0), 0, 5)
    astr2.set_attribute(libui.background_attribute(1.0, 1.0, 0.0, 0.5), 26, 36)

    font2 = {"family": "serif", "size": 16.0}
    layout2 = libui.DrawTextLayout(astr2, font2, area_w - 40)
    ctx.text(layout2, 20, 60)


async def main():
    app = App()

    app.build(
        window=Window(
            "Styled Text",
            500,
            150,
            child=VBox(stretchy(DrawArea(on_draw=on_draw))),
        )
    )

    app.show()
    await app.wait()


libui.run(main())
Styled text

Text attributes

Function

Description

weight_attribute(weight)

Font weight (e.g., TextWeight.BOLD)

italic_attribute(style)

Italic style (e.g., TextItalic.ITALIC)

color_attribute(r, g, b, a)

Text color

background_attribute(r, g, b, a)

Background highlight

underline_attribute(style)

Underline (e.g., Underline.SINGLE)

family_attribute(name)

Font family

size_attribute(size)

Font size in points

Transforms

Use DrawMatrix for translations, rotations, and scaling:

matrix = libui.DrawMatrix()
matrix.set_identity()
matrix.rotate(center_x, center_y, degrees)
ctx.save()
ctx.transform(matrix)
# ... draw rotated content ...
ctx.restore()

Mouse events

DrawArea supports mouse interaction through callbacks:

DrawArea(
    on_draw=on_draw,
    on_mouse_event=on_mouse,       # click, drag, move
    on_mouse_crossed=on_crossed,   # enter/leave
)

The mouse event dict contains: x, y, area_width, area_height, down, up, count, modifiers, held.

Call widget.queue_redraw_all() to trigger a repaint after changes.

ScrollingDrawArea

For content larger than the visible area, use ScrollingDrawArea:

ScrollingDrawArea(
    on_draw=on_draw,
    width=2000,   # virtual canvas size
    height=2000,
)