Learning Goals
3 minBy the end of this lesson you can:
- Use
on_mouse_down(pos, button),on_mouse_move(pos), andon_mouse_up(pos)to respond to mouse events. - Track a
draggingboolean flag so movement only happens while the mouse button is held. - Run a script where a shape follows the cursor only while the left mouse button is held down.
Warm-Up · What Is a Flag?
5 minIn PZ-14 you used booleans to track which keys were held. A flag is just a boolean variable you flip on and off to remember state between function calls. Predict the output:
is_dragging = False def mouse_down(): global is_dragging is_dragging = True print("drag started:", is_dragging) def mouse_up(): global is_dragging is_dragging = False print("drag ended:", is_dragging) mouse_down() mouse_up()
Show the answer
Output
drag started: True drag ended: False
The flag starts False, is set to True on press, then back to False on release. In Pygame Zero the same idea runs across three hook functions instead of one file.
New Concept · The Three Mouse Hooks
12 minThink of a drag interaction like picking up a kuih from a tray. You reach down (mouse down), slide it across (mouse move), and let go (mouse up). Three moments, three hooks.
Hook signatures
def on_mouse_down(pos, button): # called once when any mouse button is pressed # pos = (x, y) of the cursor; button = mouse.LEFT / mouse.RIGHT pass def on_mouse_move(pos): # called every time the cursor moves — even without clicking # pos = (x, y) of the cursor right now pass def on_mouse_up(pos): # called once when the mouse button is released # pos = (x, y) at release pass
The dragging pattern
Create a global flag dragging = False. Set it to True in on_mouse_down (but only if the cursor is over your target). Move the target in on_mouse_move only while dragging is True. Reset it in on_mouse_up.
dragging = False box_pos = [300, 200] # mutable list so we can change x and y def on_mouse_down(pos, button): global dragging if button == mouse.LEFT: dragging = True def on_mouse_move(pos): if dragging: box_pos[0] = pos[0] box_pos[1] = pos[1] def on_mouse_up(pos): global dragging dragging = False
Why it matters
This three-part pattern — down, move, up — is how every drag-and-drop UI on the planet works, from puzzle games to spreadsheet columns.
Worked Example · Draggable Roti
12 minThe story
Mei Ling is building a drag-the-food puzzle. She wants a roti-canai circle that follows the mouse only while the left button is held. Save as drag_roti.py:
# drag_roti.py — click and drag a circle import pgzrun WIDTH = 600 HEIGHT = 400 TITLE = "Drag the Roti" circle_x = 300 circle_y = 200 RADIUS = 40 dragging = False def draw(): screen.fill("wheat") colour = "sienna" if dragging else "peru" screen.draw.filled_circle((circle_x, circle_y), RADIUS, colour) screen.draw.text( "Hold left-click to drag", topleft=(10, 10), fontsize=22, color="saddlebrown", ) def on_mouse_down(pos, button): global dragging if button == mouse.LEFT: dx = pos[0] - circle_x dy = pos[1] - circle_y if dx * dx + dy * dy <= RADIUS * RADIUS: dragging = True # only start drag if click is on circle def on_mouse_move(pos): global circle_x, circle_y if dragging: circle_x = pos[0] circle_y = pos[1] def on_mouse_up(pos): global dragging dragging = False pgzrun.go()
What you'll see
The check dx*dx + dy*dy <= RADIUS*RADIUS is the distance formula without the slow sqrt. It returns True only when the click lands inside the circle. Actors have .collidepoint(pos) built in — you'll use that in later lessons.
Try It Yourself
13 minRemove the hit-test so the circle jumps to the cursor on any left-click, anywhere on the screen. Then drag it around freely.
Hint
def on_mouse_down(pos, button): global dragging if button == mouse.LEFT: dragging = True # no hit-test needed
Add a right-click handler: when button == mouse.RIGHT, snap the circle back to the centre of the window (300, 200).
Hint
def on_mouse_down(pos, button): global dragging, circle_x, circle_y if button == mouse.LEFT: dragging = True elif button == mouse.RIGHT: circle_x = 300 circle_y = 200
Mini-Challenge 🔥 · Drag Two Shapes
8 minCombine today's drag pattern with the list skills from Level 2. Store two circles in a list of dictionaries — each with an x, y and colour. Track which circle is being dragged using an integer index (-1 = none). The one being dragged should change colour while held.
It works if…
you can pick up and drag either circle independently without the other moving
Show one possible solution
# two_drag.py — drag two circles import pgzrun WIDTH = 600 HEIGHT = 400 RADIUS = 35 circles = [ {"x": 180, "y": 200, "colour": "tomato"}, {"x": 420, "y": 200, "colour": "dodgerblue"}, ] drag_index = -1 # -1 means nothing is dragged def draw(): screen.fill("whitesmoke") for i, c in enumerate(circles): col = "gold" if i == drag_index else c["colour"] screen.draw.filled_circle((c["x"], c["y"]), RADIUS, col) def on_mouse_down(pos, button): global drag_index if button == mouse.LEFT: for i, c in enumerate(circles): dx = pos[0] - c["x"] dy = pos[1] - c["y"] if dx * dx + dy * dy <= RADIUS * RADIUS: drag_index = i break def on_mouse_move(pos): if drag_index >= 0: circles[drag_index]["x"] = pos[0] circles[drag_index]["y"] = pos[1] def on_mouse_up(pos): global drag_index drag_index = -1 pgzrun.go()
The list + index approach scales to any number of draggable objects — just add more dicts to circles.
Recap
3 minMouse drag uses three hooks: on_mouse_down starts the drag, on_mouse_move updates the position only while a dragging flag is True, and on_mouse_up ends it. A hit-test in on_mouse_down ensures the drag only starts when the cursor lands on the target.
Vocabulary Card
- on_mouse_down(pos, button)
- Pygame Zero hook called once when a mouse button is pressed.
posis the cursor position;buttonismouse.LEFTormouse.RIGHT. - on_mouse_move(pos)
- Called every time the cursor moves. Use it with a flag to move a dragged object.
- on_mouse_up(pos)
- Called once when the mouse button is released. Clears the dragging flag.
- dragging flag
- A boolean variable that remembers whether the mouse button is currently held — bridging the gap between separate hook calls.
Homework
4 minBuild a mini jigsaw: place three coloured squares on screen. The player can drag each square to a target zone (draw the zone as an outlined rectangle). When a square is dropped inside its matching zone, print "Sesuai!" to the terminal. Save as jigsaw.py and bring a screenshot next class.
Hint: use Rect((x, y), (w, h)) and its .collidepoint(pos) to test whether the drop position is inside the zone.
Sample · jigsaw.py
# jigsaw.py — one draggable square + target zone import pgzrun WIDTH = 600 HEIGHT = 400 SIZE = 50 piece_x = 100 piece_y = 300 zone = Rect((400, 150), (80, 80)) dragging = False def draw(): screen.fill("lightyellow") screen.draw.rect(zone, "green") screen.draw.text("Drop here", topleft=(zone.x, zone.y - 20), fontsize=18, color="green") col = "gold" if dragging else "tomato" screen.draw.filled_rect(Rect((piece_x - SIZE // 2, piece_y - SIZE // 2), (SIZE, SIZE)), col) def on_mouse_down(pos, button): global dragging if button == mouse.LEFT: rx = piece_x - SIZE // 2 ry = piece_y - SIZE // 2 if Rect((rx, ry), (SIZE, SIZE)).collidepoint(pos): dragging = True def on_mouse_move(pos): global piece_x, piece_y if dragging: piece_x = pos[0] piece_y = pos[1] def on_mouse_up(pos): global dragging dragging = False if zone.collidepoint(pos): print("Sesuai!") pgzrun.go()
Extend this to three squares and three zones — just use lists like the Mini-Challenge solution. Your colours and positions will differ.