Layers¶
Menus, dialogs, tooltips, snackbars, and sheets all float over the window's content. Tesserae Engine has one mechanism for all of them: build the content from ordinary nodes and show it as a layer. Tesserae Engine stacks it, places it against an anchor, keeps it on screen, routes input around it, scopes focus to it, and tells you when the user dismisses it. What it looks like — and whether there's a scrim — is yours.
window.show_layer(node, anchor=None, placement="below", modal=False, dismissible=True,
offset=(0, 0), flip=True)
window.hide_layer(node)
The reference is Layers.
A menu¶
A menu is anchored to the button that opened it. It closes on a press outside it, on Escape, or when an item is chosen:
menu = window.create("box", width=200, padding_top=8, padding_bottom=8,
flex_direction="vertical", corner_radius=4,
fill=(0xF3, 0xED, 0xF7, 0xFF),
shadows=[((0, 0, 0, 77), 0, 1, 2, 0), ((0, 0, 0, 38), 0, 2, 6, 2)],
role="menu")
for name in ["Cut", "Copy", "Paste"]:
item = window.create("box", height=48, padding_left=12, align_items="center",
role="menuitem", label=name, focusable=True)
item.add_child(window.create("text", text=name, width=160, height=20))
item.on("click", lambda e, name=name: choose(name))
menu.add_child(item)
def choose(name):
window.hide_layer(menu)
run_command(name)
menu.on("dismiss", lambda: window.hide_layer(menu))
more_button.on("click", lambda: window.show_layer(menu, anchor=more_button))
placement picks the anchor's side: "below", "above", "start", or
"end". When that side is short of room and the opposite side has more,
the layer flips; then it shifts to stay inside the window. It re-places
itself at every layout, so it follows its anchor.
menu.get("layer_placement") says which side it landed on — for a menu
that grows upward, or an arrow that points the right way.
Corners, the centre, and an offset (0.5.6.2). placement can also name a
point of the anchor: "top_start", "top_end", "bottom_start",
"bottom_end" or "center" put the layer's centre on that point, for a badge
or a marker on a corner. A point is never flipped or shifted to fit: it stays on
the corner, even half off the window. offset=(dx, dy) moves the point the layer
attaches to, in the anchor's own coordinates (so a scaled anchor scales it;
+x is toward the end, +y is down): placement="top_end", offset=(-4, 4) sits a
badge 4 pixels in from the corner. flip=False keeps a side placement from
flipping or shifting too.
window.show_layer(badge, anchor=button, placement="top_end", offset=(-4, 4))
A layer is drawn above the whole window, escapes its ancestors' clip_children
and is its own Tab scope. For a badge or a marker that should be clipped and
ordered with its neighbours, and whose events bubble to its parent, place an
ordinary node against its anchor instead:
Anchored layout.
A press inside a layer never dismisses the layers under it, so a submenu is
just another layer, anchored to its item with placement="end".
A modal dialog¶
modal=True blocks input to everything beneath the layer and moves focus to
its first focusable node; Tab stays inside it. The scrim is an ordinary
window-sized box:
scrim = window.create("box", width="100%", height="100%", fill=(0, 0, 0, 82),
align_items="center", justify_content="center")
dialog = window.create("box", width=312, padding=24, gap=16,
flex_direction="vertical", corner_radius=28,
fill=(0xEC, 0xE6, 0xF0, 0xFF),
role="dialog", label="Discard draft?")
scrim.add_child(dialog)
# ... the dialog's text and buttons ...
scrim.on("dismiss", lambda: window.hide_layer(scrim))
window.show_layer(scrim, modal=True)
A layer without an anchor sits at its own x/y, (0, 0) by default, so
a 100% box covers the window. hide_layer detaches the layer — it stays
alive while you hold it, ready to show again — and returns focus to
whatever held it when the layer opened.
Tooltips and snackbars¶
dismissible=False leaves closing entirely to you. A tooltip shows on hover
and hides when the pointer leaves:
icon.on("pointer_enter", lambda: window.show_layer(tip, anchor=icon,
placement="above",
dismissible=False))
icon.on("pointer_leave", lambda: window.hide_layer(tip))
A snackbar is a non-dismissible layer at the bottom of the window with
live="polite", so screen readers announce it, hidden by your own timer
or its action button.
How input flows¶
- Layers stack in the order they're shown; the newest is on top.
- Events inside a layer bubble up to the layer's node and stop there — they never reach the content beneath.
- Each layer is its own Tab scope.
- With
dismissible=True, a press outside the layer — and outside its anchor — deliversdismissto the layer and goes no further, so it doesn't also click whatever was under it. Escape deliversdismissto the topmost dismissible layer. dismissasks; it doesn't close. Hide the layer, or keep it open — for a dialog with unsaved changes.
A layer isn't part of the content tree: window.root.children() doesn't
list it and its parent() is None.