Input Handling
Input in Xui is event-driven. The platform window receives raw events and routes them through the view hierarchy. All event structs are ref struct — no heap allocation on the hot path.
Source: Xui/Core/Core/UI/View.Input.cs, Xui/Core/Core/UI/Input/EventRouter.cs, Xui/Core/Core/Abstract/Events/.
Pointer events
Mouse, touch, and pen all arrive as a unified PointerEventRef. Override OnPointerEvent on any view:
public override void OnPointerEvent(ref PointerEventRef e, EventPhase phase)
{
if (e.Type == PointerEventType.Down && phase == EventPhase.Bubble)
{
var pos = e.State.Position;
// handle press at pos
}
}
Event types
PointerEventType |
W3C equivalent | When |
|---|---|---|
Over |
pointerover |
Pointer enters the hit region |
Enter |
pointerenter |
Pointer enters the element or a descendant |
Down |
pointerdown |
Button pressed |
Move |
pointermove |
Pointer moved |
Up |
pointerup |
Button released |
Cancel |
pointercancel |
System cancelled the interaction |
Out |
pointerout |
Pointer left the hit region |
Leave |
pointerleave |
Pointer left the element and all descendants |
GotCapture |
gotpointercapture |
Pointer capture acquired |
LostCapture |
lostpointercapture |
Pointer capture released |
Event phases
Every pointer event travels two phases through the view hierarchy:
- Tunnel (
EventPhase.Tunnel) — dispatched root → target. Use this to intercept events before children see them. - Bubble (
EventPhase.Bubble) — dispatched target → root. Most handlers go here.
public override void OnPointerEvent(ref PointerEventRef e, EventPhase phase)
{
if (phase == EventPhase.Tunnel)
{
// parent-first: can intercept before children
}
else // EventPhase.Bubble
{
// child-first: normal handler
}
}
Pointer state
e.State carries the full physical state of the pointer:
Point position = e.State.Position;
PointerType type = e.State.PointerType; // Mouse, Touch, Pen, Unknown
PointerButton button = e.State.Button; // Left, Right, Middle, X1, X2, Eraser
For high-frequency input (stylus drawing), use e.CoalescedStates to access all intermediate samples since the previous event.
Pointer capture
Capture keeps events flowing to your view even after the pointer leaves its bounds — useful for drag interactions:
public override void OnPointerEvent(ref PointerEventRef e, EventPhase phase)
{
if (e.Type == PointerEventType.Down && phase == EventPhase.Bubble)
CapturePointer(e.PointerId);
if (e.Type == PointerEventType.Up)
ReleasePointer(e.PointerId);
}
Scroll
public override void OnScrollWheel(ref ScrollWheelEventRef e)
{
// e.Delta is the scroll vector (positive Y = scroll up)
_offset += e.Delta;
e.Handled = true; // stop propagation to parent
InvalidateRender();
}
ScrollView handles this automatically for its content child.
Keyboard
Keyboard events reach the focused view only. Override Focusable to opt in:
public override bool Focusable => true;
public override void OnKeyDown(ref KeyEventRef e)
{
if (e.Key == VirtualKey.Return)
{
e.Handled = true;
Submit();
}
}
public override void OnChar(ref KeyEventRef e)
{
_text += e.Character;
InvalidateRender();
}
OnKeyDown fires for every key press (and auto-repeats if e.IsRepeat is true). OnChar fires for printable characters after platform key translation.
Focus API
this.Focus(); // request keyboard focus
this.Blur(); // release focus
bool f = this.IsFocused;
protected override void OnFocus() { /* show caret / highlight */ }
protected override void OnBlur() { /* hide caret / unhighlight */ }
Tab cycles focus forward through focusable views; Shift+Tab cycles backward. Focus order is managed by RootView.
Window hit testing
Override WindowHitTest in your Window subclass to define draggable and resizable regions in a custom-chrome window (no OS title bar):
public class MainWindow : Window
{
public MainWindow(IServiceProvider services) : base(services) { ... }
public override void WindowHitTest(ref WindowHitTestEventRef e)
{
// Top 48 px acts as the drag handle
if (e.Point.Y < 48)
e.Area = WindowHitTestEventRef.WindowArea.Title;
}
}
Available WindowArea values:
| Value | Effect |
|---|---|
Default |
Platform decides (transparent to framework) |
Client |
Normal content area |
Title |
Draggable title bar |
BorderTop / BorderBottom / BorderLeft / BorderRight |
Resize edge |
BorderTopLeft / BorderTopRight / BorderBottomLeft / BorderBottomRight |
Resize corner |
Transparent |
Click-through (no hit) |