Appearance
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.
| Function | Description |
|---|---|
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
endMouse & Hover Events
Mouse events are defined directly on components. Most mouse events provide the mouse coordinates (mx, my) to the callback.
| Event | Description |
|---|---|
onClick | Triggered on a left-click mouse-up action. |
onRightClick | Triggered on a right-click mouse-up action. |
onMouseEnter | Triggered when the cursor enters the node's bounds. |
onMouseLeave | Triggered 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 totrueto 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
onFocusandonBlurto 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.
| Event | Callback Arguments | Description |
|---|---|---|
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.
| Event | Callback Arguments | Description |
|---|---|---|
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" }
})
}
})
}
})
endTextInput 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 },
}),
},
}
endProperties
The TextInput accepts a properties table to customize its behavior and appearance.
| Property | Type | Description |
|---|---|---|
id | string | Unique ID for state tracking. If omitted, one is generated automatically. |
placeholder | string | Grayed-out text displayed when the input is empty. |
defaultValue | string | The initial text when the component first loads. |
value | string | For controlled components. Overrides internal state with this value. |
theme | string | Presets: "dark" (default), "light", or "high_contrast". |
onChange | function | Callback triggered on every keystroke. Receives (text). |
onSubmit | function | Callback triggered when the Enter key is pressed. Receives (text). |
style | table | Standard 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 viaonChange. - Controlled: You provide a
valueprop tied to your own state. You must update your state insideonChange, 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
})