Rendering Layers: Popups, Modals, and Tooltips in Paper
Control draw order and hit-test priority with Paper’s integer layer system. Covers Base, Overlay, and Topmost tiers and how to build modals and tooltips.
Use this file to discover all available pages before exploring further.
Paper draws and hit-tests elements in layer order. By default everything lives on layer 0 and renders in tree order, but assigning a higher layer to any element causes it to be collected during the frame and drawn last — on top of all lower-layer content — with its input hit-test priority inverted accordingly. This is the foundation for modals, dropdown menus, and tooltips.
Three named constants cover the most common tiers. They are plain int values spaced 100 apart so you can wedge custom layers between them.
public static class Layer{ public const int Base = 0; // Default. Renders first, hit-tested last. public const int Overlay = 100; // Modals, dialogs, dropdown panels. public const int Topmost = 200; // Tooltips, popovers, context menus.}
Layer.Base (0)
The default layer for all elements. Renders first, so it sits at the back. Hit-tested last, so higher layers always capture input before it does.
Layer.Overlay (100)
Intended for modal dialogs and dropdown panels. Sits above base content both visually and in the input hit-test stack.
Layer.Topmost (200)
Intended for tooltips and popovers that must float above everything, including modals. The last word in both rendering and input.
You can use any int for fine-grained control:
// A layer between Overlay and Topmost — e.g. an in-app notification bannerpaper.Box("NotificationBanner") .Layer(Layer.Overlay + 50) // = 150, above modals but below tooltips .Width(paper.Stretch()) .Height(48);
Paper’s renderer processes elements in tree order during EndFrame. When it encounters an element whose .Layer value is higher than the layer currently being rendered, it does not draw it inline. Instead it captures the element handle and its current canvas transform in a SortedDictionary keyed by layer value.After all base-layer elements have been drawn, Paper drains the dictionary in ascending key order. Each deferred element is then rendered with the transform it had when it was collected, so it appears in the correct world-space position even though it draws later. Children of a deferred element are rendered in the same deferred pass, which means an entire popup subtree — background, content, close button — all defer together and draw as a coherent unit.
EndFrame render loop: 1. Render all Layer.Base (0) elements recursively. → Any element with Layer > 0 is bucketed into deferred[layerValue]. 2. Drain deferred in ascending order: a. Render all Layer.Overlay (100) elements. → Any child with Layer > 100 goes into deferred[childLayer]. b. Render all Layer.Topmost (200) elements. c. … and so on for any custom layers.
Because the transform is captured at collection time, a tooltip that is a child of a transformed parent will still appear at the correct screen position even though it renders in a separate pass.
Elements rendered at a high layer but still parented deep inside the hierarchy can be visually correct but physically clipped by a parent with .Clip() enabled, or affected by ancestor transforms. To fully escape the parent hierarchy, call paper.MoveToRoot() while the element is the current parent:
using (paper.Box("DropdownPanel") .Layer(Layer.Overlay) .PositionType(PositionType.SelfDirected) .Left(anchorX) .Top(anchorY) .Width(220) .Enter()){ // Detach from current parent tree and re-parent under root. // The element retains its SelfDirected position so it stays // anchored at (anchorX, anchorY) in screen space. paper.MoveToRoot(); // Children are now safe from ancestor clipping foreach (var item in menuItems) { paper.Box("Item", item.ID) .Width(paper.Stretch()) .Height(36) .Text(item.Label, uiFont) .OnClick(_ => SelectItem(item)); }}
MoveToRoot() is a one-shot operation that splices the current parent out of its existing parent’s child list and re-inserts it under the root element. Call it once per frame while inside the element’s Enter() scope.
Tooltips should appear at Layer.Topmost so they are never obscured. Use MoveToRoot() so they are not clipped by the hovered element’s scroll container.