The Canvas view provides a drawing surface backed by Core Graphics. You can render shapes, lines, text, and images using declarative drawing commands from the nib.draw module.
Creating a canvas¶
import nib
canvas = nib.Canvas(width=400, height=300, background_color="#1a1a1a")
| Parameter | Type | Default | Description |
|---|---|---|---|
width |
float |
100 |
Width of the drawing area in points |
height |
float |
100 |
Height of the drawing area in points |
background_color |
str |
None |
Background color as hex string, or None for transparent |
enable_gestures |
bool |
False |
Enable pan/hover gesture tracking |
The canvas also accepts all standard view modifiers (padding, opacity, corner_radius, etc.) as keyword arguments.
Drawing commands¶
All drawing commands live in the nib.draw module. Pass a list of commands to canvas.draw():
canvas.draw([
nib.draw.Rect(x=10, y=10, width=100, height=50, fill="#3498db"),
nib.draw.Circle(cx=200, cy=100, radius=40, fill="#e74c3c"),
nib.draw.Line(x1=10, y1=200, x2=390, y2=200, stroke="#2ecc71"),
nib.draw.Text("Hello Canvas!", x=10, y=280, fill="#ffffff"),
])
Rect¶
Draws a rectangle, optionally with rounded corners:
nib.draw.Rect(
x=10, y=10,
width=100, height=80,
corner_radius=8,
fill="#3498db",
stroke="#2980b9",
stroke_width=2,
opacity=0.9,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
x |
float |
-- | X coordinate of top-left corner |
y |
float |
-- | Y coordinate of top-left corner |
width |
float |
-- | Width of the rectangle |
height |
float |
-- | Height of the rectangle |
corner_radius |
float |
0 |
Radius for rounded corners |
fill |
color/gradient | None |
Fill color or gradient |
stroke |
color | None |
Stroke color |
stroke_width |
float |
1 |
Stroke width |
opacity |
float |
1.0 |
Opacity from 0.0 to 1.0 |
blend_mode |
BlendMode |
None |
Compositing blend mode |
Circle¶
Draws a circle from a center point and radius:
nib.draw.Circle(cx=200, cy=100, radius=40, fill="#e74c3c")
| Parameter | Type | Default | Description |
|---|---|---|---|
cx |
float |
-- | X coordinate of center |
cy |
float |
-- | Y coordinate of center |
radius |
float |
-- | Circle radius |
fill |
color/gradient | None |
Fill color or gradient |
stroke |
color | None |
Stroke color |
stroke_width |
float |
1 |
Stroke width |
opacity |
float |
1.0 |
Opacity |
Ellipse¶
Draws an ellipse with independent horizontal and vertical radii:
nib.draw.Ellipse(cx=150, cy=100, rx=80, ry=40, fill="#9b59b6")
Line¶
Draws a straight line between two points:
nib.draw.Line(
x1=10, y1=10,
x2=200, y2=200,
stroke="#000000",
stroke_width=2,
line_cap="round", # "butt", "round", or "square"
)
Text¶
Draws text at a position:
nib.draw.Text(
"Hello World",
x=10, y=50,
fill="#ffffff",
font=nib.Font.system(20, weight=nib.FontWeight.BOLD),
alignment=nib.HorizontalAlignment.CENTER,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
content |
str |
-- | The text string to draw |
x |
float |
-- | X coordinate of text origin |
y |
float |
-- | Y coordinate of text origin |
fill |
color | "#000000" |
Text color |
font |
Font |
None |
Font configuration |
alignment |
HorizontalAlignment |
"left" |
Text alignment |
opacity |
float |
1.0 |
Opacity |
Arc¶
Draws an arc segment:
import math
nib.draw.Arc(
cx=100, cy=100,
radius=50,
start_angle=0,
end_angle=math.pi,
fill="#f39c12",
stroke="#e67e22",
)
Path¶
Draws a path from a list of points:
nib.draw.Path(
points=[(10, 10), (100, 50), (50, 100)],
closed=True,
fill="#2ecc71",
stroke="#27ae60",
stroke_width=2,
)
Polygon¶
A convenience wrapper around Path with closed=True:
nib.draw.Polygon(
points=[(100, 10), (190, 80), (160, 170), (40, 170), (10, 80)],
fill="#1abc9c",
)
BezierPath¶
Draws complex curves using typed path elements:
from nib.draw import BezierPath, MoveTo, CubicTo, QuadraticTo, LineTo, Close
nib.draw.BezierPath(
elements=[
MoveTo(50, 150),
CubicTo(cp1x=50, cp1y=50, cp2x=150, cp2y=50, x=150, y=150),
Close(),
],
fill="#e74c3c",
stroke="#c0392b",
)
Available path elements:
| Element | Parameters | Description |
|---|---|---|
MoveTo(x, y) |
x, y | Move to a point without drawing |
LineTo(x, y) |
x, y | Draw a straight line to a point |
CubicTo(...) |
cp1x, cp1y, cp2x, cp2y, x, y | Cubic bezier curve |
QuadraticTo(...) |
cp1x, cp1y, x, y, w | Quadratic bezier curve |
Close() |
-- | Close the path back to the start |
ArcTo(...) |
x, y, radius, rotation, large_arc, clockwise | Arc segment |
Oval(...) |
x, y, width, height | Ellipse inscribed in rectangle |
PathRect(...) |
x, y, width, height, border_radius | Rectangle sub-path |
SubPath(...) |
x, y, elements | Nested sub-path at offset |
Image¶
Draws an image from raw bytes:
with open("photo.jpg", "rb") as f:
nib.draw.Image(
data=f.read(),
x=10, y=10,
width=200,
height=150,
)
Points¶
Draws a set of points with different modes:
nib.draw.Points(
points=[(10, 10), (50, 50), (100, 30), (150, 80)],
point_mode=nib.draw.PointMode.POINTS, # POINTS, LINES, or POLYGON
stroke="#e74c3c",
stroke_width=5,
stroke_cap="round",
)
Fill¶
Fills the entire canvas with a color or gradient:
nib.draw.Fill(fill="#1a1a1a")
Colors¶
All color parameters accept hex strings or nib.Color objects:
# Hex strings
nib.draw.Rect(x=0, y=0, width=100, height=100, fill="#FF5733")
# nib.Color constants
nib.draw.Circle(cx=50, cy=50, radius=30, fill=nib.Color.RED)
Gradients¶
Fill parameters on Rect, Circle, Ellipse, BezierPath, and Fill accept gradient objects instead of solid colors:
LinearGradient¶
nib.draw.Rect(
x=0, y=0, width=200, height=100,
fill=nib.draw.LinearGradient(
start=(0, 0),
end=(200, 100),
colors=["#FF0000", "#0000FF"],
stops=[0.0, 1.0], # optional
),
)
RadialGradient¶
nib.draw.Circle(
cx=100, cy=100, radius=80,
fill=nib.draw.RadialGradient(
center=(100, 100),
radius=80,
colors=[nib.Color.YELLOW, nib.Color.RED],
),
)
SweepGradient¶
nib.draw.Circle(
cx=100, cy=100, radius=80,
fill=nib.draw.SweepGradient(
center=(100, 100),
colors=[nib.Color.RED, nib.Color.GREEN, nib.Color.BLUE, nib.Color.RED],
),
)
Drawing methods¶
The Canvas provides three methods to manage drawing commands:
canvas.draw(commands)¶
Replaces all current commands and triggers a re-render:
canvas.draw([
nib.draw.Rect(x=0, y=0, width=100, height=100, fill="#3498db"),
])
canvas.append(command)¶
Adds a single command to the existing list:
canvas.append(nib.draw.Circle(cx=50, cy=50, radius=20, fill="#e74c3c"))
canvas.clear()¶
Removes all commands and updates the display:
canvas.clear()
Gesture handling¶
Enable gestures to respond to mouse and trackpad input on the canvas. Gestures are enabled automatically when you provide any gesture callback:
canvas = nib.Canvas(
width=400, height=300,
on_pan_start=handle_start,
on_pan_update=handle_update,
on_pan_end=handle_end,
on_hover=handle_hover,
)
You can also set gesture callbacks after creation:
canvas = nib.Canvas(width=400, height=300)
canvas.on_pan_start = handle_start
canvas.on_pan_update = handle_update
PanEvent¶
All gesture callbacks receive a PanEvent dataclass with the cursor coordinates:
from nib import PanEvent
def handle_update(e: PanEvent):
print(f"x={e.x}, y={e.y}")
| Field | Type | Description |
|---|---|---|
x |
float |
X coordinate in canvas coordinates |
y |
float |
Y coordinate in canvas coordinates |
Available gesture callbacks¶
| Callback | Fired when |
|---|---|
on_pan_start |
Mouse button pressed down on canvas |
on_pan_update |
Mouse dragged while button held |
on_pan_end |
Mouse button released |
on_hover |
Mouse moves over canvas (no button held) |
Complete example: drawing app¶
A simple drawing application that lets you draw freehand lines by dragging the mouse:
import nib
def main(app: nib.App):
app.title = "Draw"
app.icon = nib.SFSymbol("pencil.tip")
app.width = 420
app.height = 380
canvas = nib.Canvas(
width=400, height=300,
background_color="#ffffff",
)
color = "#000000"
stroke_width = 3
last_pos = None
def on_start(e: nib.PanEvent):
nonlocal last_pos
last_pos = (e.x, e.y)
def on_update(e: nib.PanEvent):
nonlocal last_pos
if last_pos:
canvas.append(nib.draw.Line(
x1=last_pos[0], y1=last_pos[1],
x2=e.x, y2=e.y,
stroke=color,
stroke_width=stroke_width,
line_cap="round",
))
last_pos = (e.x, e.y)
def on_end(e: nib.PanEvent):
nonlocal last_pos
last_pos = None
canvas.on_pan_start = on_start
canvas.on_pan_update = on_update
canvas.on_pan_end = on_end
def clear_canvas():
canvas.clear()
def set_black():
nonlocal color
color = "#000000"
def set_red():
nonlocal color
color = "#e74c3c"
def set_blue():
nonlocal color
color = "#3498db"
app.build(
nib.VStack(
controls=[
canvas,
nib.HStack(
controls=[
nib.Button("Black", action=set_black),
nib.Button("Red", action=set_red),
nib.Button("Blue", action=set_blue),
nib.Spacer(),
nib.Button("Clear", action=clear_canvas),
],
spacing=8,
),
],
spacing=8,
padding=10,
)
)
nib.run(main)
Tip
Use canvas.append() inside on_pan_update for efficient incremental drawing. Using canvas.draw() with the full command list on every mouse move would be slower for complex drawings.
Reactive properties¶
Canvas dimensions and background color are reactive. Changing them triggers a re-render:
canvas.canvas_width = 500
canvas.canvas_height = 400
canvas.background_color = "#2c3e50"