Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProwlEngine/Prowl.Paper/llms.txt

Use this file to discover all available pages before exploring further.

Prowl.Paper’s event system connects raw input to your application logic through typed callbacks registered directly on the element being built. Every On… method adds a delegate to the element’s event slot and returns ElementBuilder so the chain continues uninterrupted. Events fired by the input system automatically bubble up the element tree unless propagation is explicitly stopped.

Event Bubbling

When the input system fires an event on an element, it first dispatches to that element’s registered handlers, then walks up the parent chain repeating the process until it reaches the root or propagation is stopped. The event’s Source, ElementRect, RelativePosition, and NormalizedPosition fields are retargeted to each ancestor as bubbling proceeds — Source always refers to the element currently receiving the event, not the original target. You can stop bubbling from inside a handler by calling event.StopPropagation(), or from the element declaration with StopEventPropagation() (see Behavior Modifiers).

Base Event Fields (ElementEvent)

All pointer-based events inherit from ElementEvent and expose these fields:
FieldTypeDescription
SourceElementHandleThe element currently handling this event (retargeted on bubble).
ElementRectRectLayout rectangle of Source in screen coordinates.
PointerPositionFloat2Raw cursor position in screen coordinates.
RelativePositionFloat2Cursor offset from the element’s top-left corner.
NormalizedPositionFloat2Cursor position normalized to [0, 1] within the element.
IsPropagationStoppedbooltrue after StopPropagation() has been called.

Generic Closure-Free Overloads

Every On… method has a generic overload that accepts a captured value of type T:
// Standard overload
public ElementBuilder OnClick(Action<ClickEvent> handler)

// Generic overload
public ElementBuilder OnClick<T>(T capturedValue, Action<T, ClickEvent> handler)
The generic form avoids a heap allocation that a closure would otherwise cause. Instead of capturing myValue in a lambda, you pass it as capturedValue and receive it as the first argument of the handler. This matters in frame-rate-sensitive code where OnClick is called every frame.
// ❌ Allocates a closure every frame
Paper.Element("item").OnClick(e => HandleClick(myItem, e));

// ✅ Zero closure allocation
Paper.Element("item").OnClick(myItem, (item, e) => HandleClick(item, e));
This pattern is available on every event method listed below.

Mouse Events

All mouse events carry the fields of ElementEvent plus those listed individually.

OnClick

public ElementBuilder OnClick(Action<ClickEvent> handler)
public ElementBuilder OnClick<T>(T capturedValue, Action<T, ClickEvent> handler)
Fires when the primary mouse button is pressed and released over the element. Additional ClickEvent fields:
FieldTypeDescription
ButtonPaperMouseBtnWhich mouse button triggered the event.
PhaseClickPhaseAlways ClickPhase.Click for OnClick handlers.
Paper.Element("btn")
    .OnClick(e => Console.WriteLine($"Clicked at {e.RelativePosition}"));

OnPress

public ElementBuilder OnPress(Action<ClickEvent> handler)
public ElementBuilder OnPress<T>(T capturedValue, Action<T, ClickEvent> handler)
Fires on the frame the mouse button first goes down over the element (ClickPhase.Press).

OnHeld

public ElementBuilder OnHeld(Action<ClickEvent> handler)
public ElementBuilder OnHeld<T>(T capturedValue, Action<T, ClickEvent> handler)
Fires every frame the mouse button remains held down over the element (ClickPhase.Held). Use this for continuous actions like holding a “spin up” button.

OnRelease

public ElementBuilder OnRelease(Action<ClickEvent> handler)
public ElementBuilder OnRelease<T>(T capturedValue, Action<T, ClickEvent> handler)
Fires when the mouse button is released after being pressed on this element (ClickPhase.Release). Unlike OnClick, this fires even if the cursor has moved outside the element.

OnDoubleClick

public ElementBuilder OnDoubleClick(Action<ClickEvent> handler)
public ElementBuilder OnDoubleClick<T>(T capturedValue, Action<T, ClickEvent> handler)
Fires when two clicks occur within the system double-click threshold (ClickPhase.DoubleClick).

OnRightClick

public ElementBuilder OnRightClick(Action<ClickEvent> handler)
public ElementBuilder OnRightClick<T>(T capturedValue, Action<T, ClickEvent> handler)
Fires when the secondary (right) mouse button is clicked over the element (ClickPhase.RightClick).
Paper.Element("list-item")
    .OnClick(e  => Select(item))
    .OnRightClick(e => ShowContextMenu(item, e.PointerPosition));

Drag Events

Drag events add motion-specific data on top of ElementEvent.
FieldTypeDescription
StartPositionFloat2Screen position where the drag began.
RelativePositionFloat2Current cursor position relative to the element’s top-left (inherited from ElementEvent).
DeltaFloat2Movement since the previous frame.
TotalDeltaFloat2Total movement since DragPhase.Start.
ElementRectRectElement bounds at the time of the event (inherited from ElementEvent).
PhaseDragPhaseStart, Dragging, or End.

OnDragStart

public ElementBuilder OnDragStart(Action<DragEvent> handler)
public ElementBuilder OnDragStart<T>(T capturedValue, Action<T, DragEvent> handler)
Fires once on the first frame the cursor moves while the button is held on this element.

OnDragging

public ElementBuilder OnDragging(Action<DragEvent> handler)
public ElementBuilder OnDragging<T>(T capturedValue, Action<T, DragEvent> handler)
Fires every frame while the drag is active. Use Delta for frame-relative movement (e.g. repositioning a panel).

OnDragEnd

public ElementBuilder OnDragEnd(Action<DragEvent> handler)
public ElementBuilder OnDragEnd<T>(T capturedValue, Action<T, DragEvent> handler)
Fires once when the mouse button is released after a drag. TotalDelta holds the full displacement for the whole gesture.
float panelX = 100f, panelY = 100f;

Paper.Element("panel")
    .PositionType(PositionType.SelfDirected)
    .Left(UnitValue.Pixels(panelX))
    .Top(UnitValue.Pixels(panelY))
    .OnDragging(e => {
        panelX += e.Delta.X;
        panelY += e.Delta.Y;
    });

Scroll Events

ScrollEvent inherits from ElementEvent and adds:
FieldTypeDescription
DeltafloatSigned scroll amount. Positive = scroll up / away from user.
RelativePositionFloat2Cursor position relative to the element top-left (inherited).

OnScroll

public ElementBuilder OnScroll(Action<ScrollEvent> handler)
public ElementBuilder OnScroll<T>(T capturedValue, Action<T, ScrollEvent> handler)
float scrollOffset = 0f;

Paper.Element("scroll-container")
    .Clip()
    .OnScroll(e => scrollOffset -= e.Delta * 20f);

Hover Events

Hover events use the base ElementEvent type (all pointer fields available).

OnHover

public ElementBuilder OnHover(Action<ElementEvent> handler)
public ElementBuilder OnHover<T>(T capturedValue, Action<T, ElementEvent> handler)
Fires every frame the cursor is over the element. Use RelativePosition or NormalizedPosition to track cursor location within the element.

OnEnter

public ElementBuilder OnEnter(Action<ElementEvent> handler)
public ElementBuilder OnEnter<T>(T capturedValue, Action<T, ElementEvent> handler)
Fires once on the frame the cursor first enters the element’s bounds.

OnLeave

public ElementBuilder OnLeave(Action<ElementEvent> handler)
public ElementBuilder OnLeave<T>(T capturedValue, Action<T, ElementEvent> handler)
Fires once on the frame the cursor exits the element’s bounds.
Paper.Element("tooltip-trigger")
    .OnEnter(_ => showTooltip = true)
    .OnLeave(_ => showTooltip = false);

Keyboard Events

Keyboard events are delivered only to the focused element (and its ancestors via bubbling).

OnKeyPressed

public ElementBuilder OnKeyPressed(Action<KeyEvent> handler)
public ElementBuilder OnKeyPressed<T>(T capturedValue, Action<T, KeyEvent> handler)
KeyEvent fields:
FieldTypeDescription
SourceElementHandleElement receiving the event.
KeyPaperKeyThe key that was pressed.
IsRepeatbooltrue if this is an OS key-repeat event (held key).
Paper.Element("hotkey-zone")
    .OnKeyPressed(e => {
        if (e.Key == PaperKey.Escape) CloseDialog();
        if (e.Key == PaperKey.Enter)  Submit();
    });

OnTextInput

public ElementBuilder OnTextInput(Action<TextInputEvent> handler)
public ElementBuilder OnTextInput<T>(T capturedValue, Action<T, TextInputEvent> handler)
TextInputEvent fields:
FieldTypeDescription
SourceElementHandleElement receiving the event.
CharactercharThe Unicode character produced by the key press, after IME processing.
OnTextInput receives the composed character, not the raw key. This is the correct event for any text-entry use case. Use OnKeyPressed only for non-text actions (navigation keys, shortcuts, etc.).

OnFocusChange

public ElementBuilder OnFocusChange(Action<FocusEvent> handler)
public ElementBuilder OnFocusChange<T>(T capturedValue, Action<T, FocusEvent> handler)
FocusEvent fields:
FieldTypeDescription
SourceElementHandleElement whose focus state changed.
IsFocusedbooltrue when the element gained focus; false when it lost it.
Paper.Element("search-field")
    .OnFocusChange(e => {
        if (e.IsFocused) ExpandSearchBar();
        else CollapseSearchBar();
    });

Post-Layout Callbacks

Post-layout callbacks run after the layout engine has resolved all sizes and positions, giving you access to the final Rect for drawing or measurement.

OnPostLayout

public ElementBuilder OnPostLayout(Action<ElementHandle, Rect> handler)
public ElementBuilder OnPostLayout<T>(T capturedValue, Action<T, ElementHandle, Rect> handler)
handler
Action<ElementHandle, Rect>
required
Invoked with the resolved element handle and its final screen-space Rect.
Paper.Element("measured-box")
    .Width(UnitValue.StretchOne)
    .OnPostLayout((handle, rect) => {
        Console.WriteLine($"Final width: {rect.Size.X}");
    });
The generic overload avoids closure allocation:
Paper.Element("item")
    .OnPostLayout(myData, (data, handle, rect) => Render(data, rect));

Behavior Modifiers

These methods change how the element participates in the input system rather than registering a callback.

IsNotFocusable

public ElementBuilder IsNotFocusable()
Prevents the element from ever receiving keyboard focus. Clicks still work; the element simply cannot be focused via Tab or pointer.

IsNotInteractable

public ElementBuilder IsNotInteractable()
Makes the element completely transparent to mouse and touch input. Hover, click, drag, and scroll events pass through to whatever is underneath.

StopEventPropagation

public ElementBuilder StopEventPropagation()
Prevents all events on this element from bubbling to its ancestors. Equivalent to calling event.StopPropagation() inside every handler, but declared at build time. Useful for modal panels or context menus that should not accidentally trigger parent interactions.

HookToParent

public ElementBuilder HookToParent()
Links this element’s interaction states (hovered, active, focused, dragging) to its parent’s states. When the parent is hovered the hooked child is also considered hovered and receives the same events. This enables complex hit areas where a child element logically acts as part of the parent.

TabIndex

public ElementBuilder TabIndex(int index)
index
int
required
Tab order. Elements are focused in ascending order when the user presses Tab. Use -1 (default) to exclude the element from Tab navigation.
Paper.Element("field-username").TabIndex(0);
Paper.Element("field-password").TabIndex(1);
Paper.Element("btn-submit").TabIndex(2);

Complete Example

The following example builds a draggable, closeable notification panel that demonstrates mouse events, keyboard events, event bubbling prevention, and post-layout measurement all in one element tree.
float notifX = 20f, notifY = 20f;
bool notifVisible = true;

if (notifVisible)
{
    using (Paper.Element("notification")
        .PositionType(PositionType.SelfDirected)
        .Layer(Layer.Overlay)
        .Left(UnitValue.Pixels(notifX))
        .Top(UnitValue.Pixels(notifY))
        .Size(UnitValue.Pixels(320), UnitValue.Pixels(80))
        .Rounded(8f)
        .BackgroundColor(Color32.FromArgb(240, 30, 30, 46))
        .StopEventPropagation()                          // don't bubble clicks to the world
        .OnDragging(e => { notifX += e.Delta.X; notifY += e.Delta.Y; })
        .OnPostLayout((_, rect) => Console.WriteLine($"Notif at {rect.Min}"))
        .Enter())
    {
        Paper.Element("close-btn")
            .PositionType(PositionType.SelfDirected)
            .Right(UnitValue.Pixels(8))
            .Top(UnitValue.Pixels(8))
            .Size(UnitValue.Pixels(20))
            .Rounded(10f)
            .OnClick(_ => notifVisible = false);
    }
}

Build docs developers (and LLMs) love