Skip to content

Input & Interactivity

Vulpis provides a robust system for handling user input, ranging from low-level keyboard polling to complex declarative event handlers for mouse interactions and drag-and-drop.

Keyboard Polling

Polling allows you to check the real-time state of the keyboard during every frame (on_tick). This is ideal for hotkey-based actions.

FunctionDescription
vulpis.isKeyHeld(keyName)Returns true if the key is currently being pressed down.
vulpis.isKeyJustPressed(keyName)Returns true only on the exact frame the key was first pressed.

Key Naming Convention: Vulpis uses standard SDL Scancode names for key polling. The strings are case-sensitive:

  • Letters are strictly Uppercase: "A", "W", "S", "D".
  • Special Keys use Title Case: "Escape", "Space", "Return", "Backspace", "Up", "Down", "Left", "Right", "LShift", "RShift".
  • Numbers are standard digits: "1", "2", "0".

Example:

lua
function on_tick(dt)
    if vulpis.isKeyJustPressed("Escape") then
      print("Menu toggled!")
    end
    
    if vulpis.isKeyHeld("W") then
      print("w pressed")
    end
end

Mouse & Hover Events

Mouse events are defined directly on components. Most mouse events provide the mouse coordinates (mx, my) to the callback.

EventDescription
onClickTriggered on a left-click mouse-up action.
onRightClickTriggered on a right-click mouse-up action.
onMouseEnterTriggered when the cursor enters the node's bounds.
onMouseLeaveTriggered when the cursor exits the node's bounds.

Mouse Event Examples

Vulpis mouse events provide coordinates and interaction data directly to your callback functions. Below are specific examples for every supported mouse event.

onClick (Left Click)

Triggered when the left mouse button is released over a node. It provides the exact screen coordinates of the click.

lua
el.Box({
    style = { w = 200, h = 50, BGColor = "#4CAF50" },
    onClick = function(mx, my)
        print("Left clicked at X: " .. mx .. " Y: " .. my)
    end
})

onRightClick

Triggered when the right mouse button is released over a node. This is ideal for triggering context menus.

lua
el.Box({
    style = { w = 200, h = 50, BGColor = "#2196F3" },
    onRightClick = function(mx, my)
        print("Right clicked! Opening context menu at: " .. mx .. ", " .. my)
    end
})

onMouseEnter

Fired the moment the mouse cursor crosses the boundary into the node.

lua
el.Box({
    style = { w = 200, h = 50, BGColor = "#333333" },
    onMouseEnter = function()
        print("Mouse entered the box area")
    end
})

onMouseLeave

Fired the moment the mouse cursor moves outside the node's bounding box.

lua
el.Box({
    style = { w = 200, h = 50, BGColor = "#333333" },
    onMouseLeave = function()
        print("Mouse left the box area")
    end
})

Note on Coordinates: The mx and my arguments represent the mouse position in global window coordinates. If your node is inside a scrolled container, the engine automatically handles the hit-testing logic for you.

Focus Management

Focus allows components to capture keyboard events specifically. This is a prerequisite for text input components.

  • focusable: Set this property to true to allow the node to receive focus when clicked.
  • isFocused: A property you can read (or set manually) to check the current focus state.
  • Events: Use onFocus and onBlur to trigger logic when focus is gained or lost.

Example:

lua
el.Box({
    focusable = true,
    style = { w = 200, h = 40, BGColor = "#222222" },
    onFocus = function() print("I have the focus!") end,
    onBlur = function() print("Focus lost.") end
})

Text Input & Typing

To capture typing, a node must be focusable. Vulpis provides two distinct events for handling the keyboard.

EventCallback ArgumentsDescription
onTextInput(char)Captures the actual character typed (respects layout/caps).
onKeyDown(keyName, mods)Captures physical key presses and modifier states.

Key Names in onKeyDown: The keyName argument returns the SDL Key name. Just like polling, expect special keys to be Title Case (e.g., "Return", "Backspace", "Delete", "Home", "End"). For letters, it is safe practice to check for both cases (e.g., key == "a" or key == "A") if you are building custom input handlers.

Modifier Table (mods): The mods argument in onKeyDown is a table containing boolean values for: ctrl, shift, alt, and gui.

Example:

lua
el.Text({
    text = "Type here...",
    focusable = true,
    onTextInput = function(char)
        print("User typed: " .. char)
    end,
    onKeyDown = function(key, mods)
        if key == "Return" and mods.ctrl then
            print("Ctrl + Enter pressed!")
        end
    end
})

Drag & Drop System

The drag-and-drop system is built into the Vulpis VDOM core. You can enable automatic visual movement or use raw event data to track drag progress manually.

Core Attributes

  • draggable = true: Enables the built-in engine behavior where the node visually follows the mouse cursor while dragged. The engine automatically applies the visual offset for you.
  • id: To make a node a valid Drop Target, you must assign it a unique string ID.

Drag Events

Event callbacks provide real-time coordinates and interaction data from the engine.

EventCallback ArgumentsDescription
onDragStart(mx, my, textIdx, clicks)Fired when the user first clicks and initiates a move.
onDrag(dx, dy, mx, my, textIdx)Fired every frame during the drag. Provides distance from start (dx/dy) and current cursor position (mx/my).
onDragEnd(dropTargetId, dx, dy)Fired upon release. dropTargetId is the id of the topmost node under the cursor.

Comprehensive Example: Drag-to-Delete

This example demonstrates a draggable item that changes its appearance while moving and interacts with a specific drop zone.

lua
local el = require("utils.core.elements")

function App()
    -- Track if we are currently dragging to change the trash can color
    local is_dragging = useState("dragging_item", false)

    return el.HBox({
        style = { gap = 50, padding = 20 },
        children = {
            -- 1. THE DRAGGABLE ITEM
            el.Box({
                id = "file-icon",
                draggable = true, -- Engine handles the visual movement
                style = { 
                    w = 60, h = 60, 
                    bg = "#61afef", 
                    rounded = 10 
                },
                onDragStart = function()
                    setState("dragging_item", true)
                end,
                onDragEnd = function(dropTargetId, dx, dy)
                    setState("dragging_item", false)
                    
                    -- Check if we dropped it on the trash can
                    if dropTargetId == "trash-bin" then
                        print("Item deleted!")
                        -- Add logic here to remove item from your state
                    end
                end,
                children = { el.Text("FILE") }
            }),

            -- 2. THE DROP ZONE (Target)
            el.Box({
                id = "trash-bin", -- Required for onDragEnd to detect it
                style = {
                    w = 100, h = 100,
                    -- Highlight if an item is being dragged anywhere
                    bg = is_dragging and "#e06c75" or "#333333",
                    center = true,
                    rounded = 20
                },
                children = {
                    el.Text({ 
                        text = "TRASH", 
                        style = { FontColor = "#ffffff" } 
                    })
                }
            })
        }
    })
end

TextInput Component

The TextInput component is a high-level utility found in utils/core/textInput.lua. It provides a fully-featured text entry field with support for themes, focus states, cursor navigation, and standard clipboard shortcuts.

Basic Usage

To use it, you must require the utility module. Unlike standard elements, TextInput manages its own internal state for cursor position and selection.

lua
local TextInput = require("utils.core.textInput")

function App()
	return {
		type = "vbox",
		style = {
			padding = 10,
		},
		children = {
			TextInput({
				placeholder = "Type your name...",
				theme = "high_contrast",
				onChange = function(text)
					print("Current text: " .. text)
				end,
				onSubmit = function(text)
					print("User pressed Enter: " .. text)
				end,
				style = { w = 300, h = 40 },
			}),
		},
	}
end

Properties

The TextInput accepts a properties table to customize its behavior and appearance.

PropertyTypeDescription
idstringUnique ID for state tracking. If omitted, one is generated automatically.
placeholderstringGrayed-out text displayed when the input is empty.
defaultValuestringThe initial text when the component first loads.
valuestringFor controlled components. Overrides internal state with this value.
themestringPresets: "dark" (default), "light", or "high_contrast".
onChangefunctionCallback triggered on every keystroke. Receives (text).
onSubmitfunctionCallback triggered when the Enter key is pressed. Receives (text).
styletableStandard style overrides (width, height, margins, etc.).

Controlled vs. Uncontrolled

  • Uncontrolled (Default): You provide a defaultValue. The component manages the string internally, and you listen for changes via onChange.
  • Controlled: You provide a value prop tied to your own state. You must update your state inside onChange, or the input will appear "locked" because it will always render the value you pass it.
lua
-- Controlled Example
local val = useState("my_input", "")

TextInput({
    value = val,
    onChange = function(new_text)
        setState("my_input", new_text)
    end
})