Events and Listeners¶
Listeners registered with on(event, handler) on a Node or a
Window. For a walkthrough, see
Events and Input.
Node listeners¶
button.on("click", lambda e: print("clicked", e.target == button))
button.off("click")
node.on(event, handler) registers handler, replacing any earlier listener
for the same event on that node. handler receives an Event, or
nothing if it takes no parameters. An unknown event name raises ValueError
listing the valid ones.
| Event | Fires when | Bubbles |
|---|---|---|
pointer_enter, pointer_leave |
The pointer enters or leaves the node's subtree — not when it moves between the node and its own descendants, and pointer_leave also fires when the pointer leaves the window |
no |
pointer_down, pointer_move, pointer_up |
A button is pressed, the pointer moves, a button is released | yes |
pointer_cancel |
A press was taken by the OS — to move the window from a drag region, toggle maximize, or show the window menu — so no pointer_up or click follows, and any pointer capture is released (0.5.0) |
yes |
click |
Primary press and release on the same node, or keyboard activation | yes |
secondary_click |
Secondary press and release on the same node | yes |
wheel |
A wheel or trackpad scroll | yes |
file_hover, file_hover_cancel, file_drop |
Files dragged from the OS over, away from, or dropped on the node under the pointer; paths (0.5.4) |
yes |
link |
A click (press and release) on a text span that has a link; href is the span's link string. Not fired when the press became a selection (0.5.4) |
yes |
touch_start, touch_move, touch_end, touch_cancel |
A finger touches the screen, moves, lifts, or is taken away by the system; each carries pointer_id, and a finger's events all go to the node it landed on (0.5.4) |
yes |
tap |
A quick touch and release in place; count is 2 for a second tap close to the first (0.5.4) |
yes |
long_press |
A touch held in place for half a second (0.5.4) | yes |
pan |
One finger dragging past a small slop; phase is "began", "changed", "ended" or "cancelled" (0.5.4) |
yes |
pinch |
Two fingers moving together or apart, or a trackpad pinch; carries scale and scale_delta (0.5.4) |
yes |
key_down, key_up |
A key is pressed or released while the node, or a descendant, has focus (the root gets keys when nothing is focused) | yes |
input |
Committed text arrives for the focused text field | yes |
focus, unfocus |
A node gains or loses keyboard focus | yes |
change |
A text field's text was changed by the user | no |
scroll |
A scroll view's offset changed, by any cause: wheel, keys, scroll_into_view, focus, set, or an animation, once a frame (0.4.2) |
no |
animation_end |
An animation on the node ended: it ran to its end (finished is True), or it was replaced by another animation of the same property, or the property was set or stop_animationped (finished is False); property names it. Delivered in the frame after, and after any on_complete callback (0.5.6) |
no |
submit |
Enter in a single-line text input (0.5.6) | no |
compose_start, compose_update, compose_end |
An IME composition began, changed or ended in a text input; text is the preedit text and preedit_cursor the IME's cursor range. The commit follows as input and change (0.5.6) |
no |
caret_move |
A text input's caret moved or its text changed by a key, click or drag; caret is its rectangle and inserted the bytes an edit inserted. Not for set (0.5.6) |
no |
layout |
The node's laid-out size changed, or it was laid out for the first time, by any cause (a set, a parent's resize, a calc size, new text); width/height are the new size and old_value/new_value the (width, height) before (None the first time) and after. Only a node with a listener is watched; once a frame (0.5.6.2) |
no |
dismiss |
An outside press or Escape asked a layer to close | no |
a11y_action |
An assistive technology requested increment, decrement, expand, collapse, scroll_into_view, or set_value (its activate and focus requests arrive as click and focus) |
yes |
Propagation¶
A bubbling event runs the target's listener, then its parent's, and so on to
the root. Any listener can call event.stop() to end it. event.target is
where the event happened and event.current is the node whose listener is
running. Because focus and unfocus bubble, a container knows focus moved
somewhere inside it, and event.target says where.
Focus¶
focus and unfocus each carry event.related_target: the node on the other
side of the move. It's the node losing focus for focus and the node gaining
it for unfocus. It's None when focus comes from, or goes to, nowhere in the
window. A search bar wrapping a text field and a clear button can use it to
tell focus moving between its own children from focus leaving it:
def on_unfocus(e):
if e.related_target not in (field, clear_button):
close_suggestions()
bar.on("unfocus", on_unfocus)
focus also carries event.focus_visible, which says whether to draw a focus
indicator (Tesserae Engine draws none itself):
Truewhen focus arrived by keyboard: Tab, Shift+Tab, or an assistive technology's focus request.Falsewhen focus arrived from a pointer press.- For
node.focus()and other programmatic focus, whatever the last interaction was. Before any pointer press, that'sTrue.
Key presses with Ctrl, Alt, or Meta held are shortcuts, not navigation, so they
don't count as keyboard input for this. This is the heuristic browsers use for
:focus-visible.
Raw input is delivered before what it caused, so a click delivers
pointer_down, then pointer_up, then click.
text nodes are never the target of pointer events: a press on a button's
label lands on the button. Set hit_testable=False on an icon (a path) for
the same effect.
Pointer capture¶
def on_down(e):
thumb.capture_pointer()
thumb.on("pointer_down", on_down)
thumb.on("pointer_move", lambda e: print(e.x)) # keeps firing off the thumb
node.capture_pointer() routes every later pointer event to node, wherever
the pointer goes, until a button is released or node.release_pointer() is
called. While captured, the capturing node is the target and events still
bubble from it.
Window listeners¶
window.on("resize", lambda e: print(e.width, e.height))
window.on("close_requested", lambda e: e.cancel()) # keep the window open
| Event | Fires when | Payload |
|---|---|---|
resize |
The window's client area changed size | width, height |
color_scheme |
The OS switched between light and dark | dark |
frame |
A frame was drawn and presented (0.5.4); fires every frame, so keep the handler cheap | stats |
reduced_motion |
The OS's reduce-motion preference changed (0.5.4) | reduced_motion |
high_contrast |
The OS's increased-contrast preference changed (0.5.4) | high_contrast |
scale_factor |
The window moved to a display with a different scale factor | scale_factor |
close_requested |
The user asked to close the window; event.cancel() keeps it open |
— |
closed |
The window closed — by the user or by reaching max_frames |
— |
dock_target |
During a docking drag, the pointer moved into another dock zone, or out of every zone | side |
file_hover |
Files are being dragged from the OS over the window (0.5.4); paths |
— |
file_hover_cancel |
The file drag left the window or was abandoned (0.5.4) | — |
file_drop |
Files were dropped on the window (0.5.4); paths |
— |
dock_drop |
A docking drag ended with the primary button's release; the panel has moved to the zone there, if any | panel, side |
maximized |
The window was maximized or restored (0.5.0) | maximized |
active |
The window gained or lost the OS's focus (0.5.0) | active |
titlebar_inset |
The area the OS's window controls take over the content changed — macOS's traffic lights on an undecorated window, 0 in fullscreen (0.5.0) | titlebar_inset |
gpu_lost |
The GPU was lost — a driver fault, or a hang the driver's own timeout caught — and the run is ending (0.5.1) | reason ("unknown" or "destroyed"), message |
gpu_error |
The GPU reported an error; the draw that caused it was skipped. Once per distinct message (0.5.1) | message |
gpu_stalled |
A submitted frame hasn't completed after gpu_watchdog seconds (0.5.1) |
seconds |
A window event has no node: event.target is None.
Window properties¶
window.set(title="Editor — draft.md")
window.get("scale_factor") # 1.0 until App.run() opens the window
window.get("dark") # True, False, or None where the OS can't say
window.get("reduced_motion") # (0.5.4) True if the user asked for less motion
window.get("high_contrast") # (0.5.4) True if the user asked for more contrast
window.set(title=...) changes the title, live if the window is open.
window.set(partial_redraw=False) redraws the whole window every frame
instead of only what changed (Window). window.get(name) reads
width, height, title, scale_factor, dark, reduced_motion, high_contrast, partial_redraw,
partial_redraw_active, or show_damage, and (0.5.0) decorations,
fullscreen, min_width, min_height, maximized, minimized, active,
platform, resize_border, system_menu, titlebar_inset,
native_controls, and (0.5.1) gpu_watchdog — see
Window controls and state,
macOS: the overlay title bar, and
Title bar and borders.
root is the window's root node.
dark is the OS's appearance right now, so an app that follows it can start
in the right one and then listen to color_scheme. Linux reads it from the
desktop's settings portal, before App.run() too, and the portal's changes
arrive as color_scheme; macOS and Windows answer once the window is open,
and None before. None also means no portal answered (a headless session).
Reduced motion and increased contrast (0.5.4) are the OS's accessibility
preferences, read the same way. window.get("reduced_motion") is True when the
user asked the system to cut animation, False when they did not, and None where
the platform can't say; get("high_contrast") is the same for "more contrast". An
app that honours them reads them at start and listens for reduced_motion and
high_contrast:
if window.get("reduced_motion"):
ANIMATE_MS = 0 # snap instead of easing
window.on("reduced_motion", lambda e: set_animations(not e.reduced_motion))
window.on("high_contrast", lambda e: use_high_contrast_palette(e.high_contrast))
The engine reports them and changes nothing itself: durations are the app's to
shorten, and colours its to swap. Linux reads the settings portal's
reduced-motion and contrast (with GNOME's enable-animations and
high-contrast for an older portal) and its changes arrive live; Windows reads the
client-area animation setting and the High Contrast theme, macOS the "Reduce motion"
and "Increase contrast" options. Windows and macOS announce no change, so the event
fires when the window regains focus after one. Only the Linux read is checked on real
hardware; the Windows and macOS code is compiled for those targets and untested.
Event¶
| Field | Present for |
|---|---|
type |
every event |
target |
every node event — where it happened |
current |
node listener events — the node whose listener is running |
x, y |
pointer and wheel events — local to current |
window_x, window_y |
pointer and wheel events — in the window |
button |
pointer events and pointer clicks — "primary", "secondary", "middle", or the mouse's side buttons "back" and "forward" (0.4.1; they make no click) |
delta_x, delta_y |
wheel — pixels, positive scrolling right and down; with Shift held, a wheel with no horizontal part arrives as delta_x (0.4.3) |
pointer_id |
touch_* — which finger |
phase |
pan, pinch — "began", "changed", "ended", "cancelled" |
count |
tap — 1, or 2 for a double tap |
scale, scale_delta |
pinch — the distance between fingers over what it was at the start, and over what it was at the last event |
delta_x, delta_y |
(also) pan, pinch — how far the point (a pan's finger, a pinch's midpoint) moved since the last event |
total_x, total_y |
pan, pinch — how far it has moved in all |
velocity_x, velocity_y |
pan when it ends — the speed the finger lifted at, in pixels a second |
key, repeat |
key_down, key_up |
shift, ctrl, alt, meta |
pointer, wheel, key, and click events |
text |
input |
old_value, new_value |
change — the text; scroll — the offset; layout — the (width, height) size before (None the first time) and after |
action, value |
a11y_action |
related_target |
focus, unfocus — the node on the other side of the move |
focus_visible |
focus — whether focus arrived by keyboard |
width, height / dark / scale_factor |
resize / color_scheme / scale_factor (and layout on a node: its new size) |
reduced_motion / high_contrast |
reduced_motion / high_contrast — the new preference |
stats |
frame — the frame's costs, the same dict as window.frame_stats()["last"] |
maximized / active |
maximized / active — the window's new state |
path, paths |
file_hover, file_drop — the dragged files' paths as strs; path is the first |
href |
link — the clicked span's link string |
preedit_cursor |
compose_start, compose_update — the IME's (start, end) cursor range in the preedit text, or None |
caret, inserted |
caret_move — the caret's (x, y, width, height) in the node's coordinates, and the (start, end) bytes an edit inserted or None |
property, finished |
animation_end — the animated property, and whether it ran to the end (False: it was replaced or stopped) |
side |
dock_target, dock_drop — the dock zone under the pointer, or None |
panel |
dock_drop — the dragged panel |
Methods: stop() ends propagation; cancel() prevents close_requested's
default (any other event raises ValueError).
Key names are snake_case ("enter", "escape", "arrow_left", "page_up",
"f5") for named keys, and the produced character ("a", or "A" with Shift)
for character keys.
Node handles compare equal, and hash equal, when they name the same node —
event.target == button works, and nodes can key a dict.
Testing without a display¶
window.simulate(event, node=None, **fields) delivers a synthetic event through
the same pipeline as real input, so every listener fires as it would live.
window.simulate("click", node=button)
window.simulate("pointer_down", node=thumb, x=4, y=10, shift=True)
window.simulate("key_down", key="tab")
window.simulate("resize", width=800, height=600)
| Event | Fields |
|---|---|
pointer_down, pointer_up |
node and/or x, y; button |
pointer_move, pointer_enter, click, secondary_click |
node and/or x, y |
wheel |
node and/or x, y; delta_x, delta_y |
pointer_leave |
— (the pointer leaves the window) |
file_hover, file_drop |
paths (a list of str) or path; node and/or x, y for where they are over |
link |
node and href (a str); delivers the event as a click on a link would |
file_hover_cancel |
— |
touch_start, touch_move, touch_end, touch_cancel |
node and/or x, y; id (default 0) tells fingers apart |
trackpad_pinch |
node and/or x, y; delta (a magnification step, 1 + delta the scale to apply), phase ("started", "moved" — the default — "ended", "cancelled") |
key_down, key_up |
key; repeat |
input |
text |
focus, unfocus |
node |
a11y_action |
node, action; value for set_value |
resize |
width, height |
color_scheme |
dark |
reduced_motion, high_contrast |
value (a bool) |
scale_factor |
scale_factor |
close_requested, closed |
— |
maximized / active |
maximized / active — sets the window's state; the event fires only if it changed, as the live window's does. Before App.run(), maximize()/restore()/minimize() change the state with no event, so simulating the state you're already in fires nothing |
Pointer events aim at node's center, at x/y local to node, or at
window-space x/y without a node. Pointer, wheel, click, and key events also
take shift, ctrl, alt, and meta. A simulated key press edits text and moves focus exactly
as a real one does — Ctrl+C/X/V/A included — and a focused terminal takes
every key, as it does live. Simulated pointer, touch and key events also reach the
parts of text handling that need the text layout (0.5.4): a press, drag and release
select text, a press and release on a link span fire link, two quick presses select a
word, and Shift+Up and Shift+Down move a selection a line, all as they do live (the text
layout comes from a headless renderer, and the clock is the window's, so advance moves
the double-click window). An unknown event or field raises ValueError.