Update Input and Events wiki with current API: Key/InputState/MouseButton enums, scene.on_key, Vector-based callbacks

John McCardle 2026-02-07 22:15:28 +00:00
commit 222ee83009

@ -1,162 +1,264 @@
# Input and Events # Input and Events
McRogueFace provides keyboard, mouse, and window event handling through Python callbacks. Events are dispatched through the scene system, allowing different scenes to have different input handlers. McRogueFace provides keyboard, mouse, and window event handling through Python callbacks. Events are dispatched through the scene system, allowing different scenes to have different input handlers.
**Related Pages:** **Related Pages:**
- [[UI-Widget-Patterns]] - Modal dialogs, buttons, hotbars - [[UI-Widget-Patterns]] - Modal dialogs, buttons, hotbars
- [[Grid-Interaction-Patterns]] - Entity selection, movement, context menus - [[Grid-Interaction-Patterns]] - Entity selection, movement, context menus
- [[Writing-Tests]] - Testing input with automation API - [[Writing-Tests]] - Testing input with automation API
**Key Files:** **Key Files:**
- `src/GameEngine.cpp::processEvent()` - Event dispatch - `src/GameEngine.cpp::processEvent()` - Event dispatch
- `src/Scene.cpp::sUserInput()` - Scene input handling - `src/PyScene.cpp` - Scene input handling
- `src/McRFPy_API.cpp` - Python callback registration - `src/PyCallable.cpp` - Callback wrappers (PyClickCallable, PyHoverCallable, PyCellCallable)
--- ---
## Keyboard Input ## Keyboard Input
Register a scene-level callback that fires on every key press/release: Register a keyboard handler on a Scene object. The callback receives enum values, not strings:
```python ```python
import mcrfpy import mcrfpy
def handle_key(key: str, pressed: bool): scene = mcrfpy.Scene("game")
"""
key: Key name (e.g., "W", "Space", "Escape", "Up") def handle_key(key, action):
pressed: True on press, False on release """
""" key: mcrfpy.Key enum (e.g., Key.W, Key.ESCAPE)
if key == "Escape" and pressed: action: mcrfpy.InputState enum (PRESSED or RELEASED)
mcrfpy.setScene("menu") """
if key == mcrfpy.Key.ESCAPE and action == mcrfpy.InputState.PRESSED:
mcrfpy.keypressScene(handle_key) menu_scene.activate()
``` elif key == mcrfpy.Key.W and action == mcrfpy.InputState.PRESSED:
player_move(0, -1)
### Key Names
scene.on_key = handle_key
Common key names from SFML: mcrfpy.current_scene = scene
- Letters: `"A"` through `"Z"` ```
- Numbers: `"Num0"` through `"Num9"`
- Arrows: `"Up"`, `"Down"`, `"Left"`, `"Right"` ### Key Enum (`mcrfpy.Key`)
- Special: `"Space"`, `"Enter"`, `"Escape"`, `"Tab"`, `"LShift"`, `"RShift"`, `"LControl"`, `"RControl"`
- Function: `"F1"` through `"F12"` All keyboard keys are available as attributes of the `mcrfpy.Key` enum:
--- - **Letters:** `Key.A` through `Key.Z`
- **Numbers:** `Key.Num0` through `Key.Num9`
## Mouse Input - **Arrows:** `Key.UP`, `Key.DOWN`, `Key.LEFT`, `Key.RIGHT`
- **Special:** `Key.SPACE`, `Key.ENTER`, `Key.ESCAPE`, `Key.TAB`, `Key.BACKSPACE`, `Key.DELETE`
### Element Event Handlers - **Modifiers:** `Key.LSHIFT`, `Key.RSHIFT`, `Key.LCTRL`, `Key.RCTRL`, `Key.LALT`, `Key.RALT`
- **Function:** `Key.F1` through `Key.F15`
All UIDrawables support mouse event callbacks:
### InputState Enum (`mcrfpy.InputState`)
| Property | Signature | When Called |
|----------|-----------|-------------| | Value | Meaning |
| `on_click` | `(x, y, button) -> None` | Mouse button pressed on element | |-------|---------|
| `on_enter` | `() -> None` | Mouse enters element bounds | | `InputState.PRESSED` | Key/button was just pressed down |
| `on_exit` | `() -> None` | Mouse leaves element bounds | | `InputState.RELEASED` | Key/button was just released |
| `on_move` | `(x, y) -> None` | Mouse moves within element |
**Legacy compatibility:** Enum values support string comparison for backwards compatibility: `InputState.PRESSED == "start"` returns `True`. However, using the enum directly is preferred.
```python
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 50)) ---
frame.on_click = lambda x, y, btn: print(f"Clicked at ({x}, {y}) with button {btn}") ## Mouse Input
frame.on_enter = lambda: print("Mouse entered")
frame.on_exit = lambda: print("Mouse exited") ### Element Click Handlers
```
All UIDrawable elements (Frame, Caption, Sprite, Grid, Line, Circle, Arc) support mouse event callbacks:
The `hovered` property (read-only) indicates whether the mouse is currently over an element.
| Property | Signature | When Called |
### Grid Cell Events |----------|-----------|-------------|
| `on_click` | `(pos: Vector, button: MouseButton, action: InputState)` | Mouse button pressed/released on element |
Grids provide cell-level mouse events in addition to element events: | `on_enter` | `(pos: Vector)` | Mouse enters element bounds |
| `on_exit` | `(pos: Vector)` | Mouse leaves element bounds |
| Property | Signature | Description | | `on_move` | `(pos: Vector)` | Mouse moves within element |
|----------|-----------|-------------|
| `on_cell_click` | `(grid_x, grid_y, button) -> None` | Cell clicked | ```python
| `on_cell_enter` | `(grid_x, grid_y) -> None` | Mouse enters cell | frame = mcrfpy.Frame(pos=(100, 100), size=(200, 50))
| `on_cell_exit` | `(grid_x, grid_y) -> None` | Mouse leaves cell |
| `hovered_cell` | `(x, y)` or `None` | Currently hovered cell (read-only) | def on_frame_click(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
See [[Grid-Interaction-Patterns]] for usage examples. print(f"Left-clicked at ({pos.x}, {pos.y})")
elif button == mcrfpy.MouseButton.RIGHT and action == mcrfpy.InputState.PRESSED:
### Mouse Position print("Right-clicked!")
```python frame.on_click = on_frame_click
# Window coordinates (pixels) frame.on_enter = lambda pos: print(f"Mouse entered at ({pos.x}, {pos.y})")
x, y = mcrfpy.getMousePos() frame.on_exit = lambda pos: print("Mouse exited")
``` frame.on_move = lambda pos: print(f"Mouse at ({pos.x}, {pos.y})")
```
---
The `hovered` property (read-only) indicates whether the mouse is currently over an element.
## Event Priority
### MouseButton Enum (`mcrfpy.MouseButton`)
### Click Dispatch Order
| Value | Meaning |
Clicks are dispatched in reverse render order (front to back): |-------|---------|
| `MouseButton.LEFT` | Left mouse button |
1. **UI elements with highest z_index** receive clicks first | `MouseButton.RIGHT` | Right mouse button |
2. If handled (callback returns truthy), propagation stops | `MouseButton.MIDDLE` | Middle mouse button (scroll wheel click) |
3. **Entities on grids** receive clicks next | `MouseButton.SCROLL_UP` | Scroll wheel up |
4. **Grid cells** receive clicks last | `MouseButton.SCROLL_DOWN` | Scroll wheel down |
### Keyboard Priority ### Grid Cell Events
Keyboard events go only to the current scene's registered callback. There is no concept of "focused" UI elements for keyboard input. Grids provide cell-level mouse events with grid coordinates:
--- | Property | Signature | Description |
|----------|-----------|-------------|
## Window Events | `on_cell_click` | `(cell_pos: Vector, button: MouseButton, action: InputState)` | Cell clicked |
| `on_cell_enter` | `(cell_pos: Vector)` | Mouse enters cell |
### Resize Events | `on_cell_exit` | `(cell_pos: Vector)` | Mouse leaves cell |
| `hovered_cell` | `(x, y)` tuple or `None` | Currently hovered cell (read-only) |
Window resize events are not currently exposed directly. Workaround using timer polling:
```python
```python grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(400, 300))
last_size = mcrfpy.getWindowSize()
def on_cell_click(cell_pos, button, action):
def check_resize(dt): if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
global last_size x, y = int(cell_pos.x), int(cell_pos.y)
current = mcrfpy.getWindowSize() point = grid.at(x, y)
if current != last_size: print(f"Cell ({x}, {y}): walkable={point.walkable}")
on_resize(current)
last_size = current grid.on_cell_click = on_cell_click
grid.on_cell_enter = lambda cell_pos: print(f"Entered cell ({cell_pos.x}, {cell_pos.y})")
mcrfpy.setTimer("resize_check", check_resize, 100) grid.on_cell_exit = lambda cell_pos: print(f"Left cell ({cell_pos.x}, {cell_pos.y})")
``` ```
--- See [[Grid-Interaction-Patterns]] for usage examples.
## Testing Input ---
Use the automation API to simulate input in tests: ## Event Priority
```python ### Click Dispatch Order
from mcrfpy import automation
Clicks are dispatched in reverse render order (front to back):
automation.keypress("W", True) # Press W
automation.keypress("W", False) # Release W 1. **UI elements with highest z_index** receive clicks first
automation.click(100, 200, button=0) # Left-click at position 2. If handled (callback exists), propagation stops
``` 3. **Entities on grids** receive clicks next
4. **Grid cells** receive clicks last
See [[Writing-Tests]] for complete testing patterns.
### Keyboard Priority
---
Keyboard events go only to the current scene's `on_key` handler. There is no concept of "focused" UI elements for keyboard input.
## API Reference
---
**Module Functions:**
- `mcrfpy.keypressScene(callback)` - Register keyboard handler ## Common Patterns
- `mcrfpy.getMousePos() -> (int, int)` - Get mouse position
- `mcrfpy.getWindowSize() -> (int, int)` - Get window dimensions ### WASD Movement
**UIDrawable Properties:** ```python
- `on_click`, `on_enter`, `on_exit`, `on_move` - Event handlers scene = mcrfpy.Scene("game")
- `hovered` - Mouse hover state (read-only)
def handle_key(key, action):
**Grid Properties:** if action != mcrfpy.InputState.PRESSED:
- `on_cell_click`, `on_cell_enter`, `on_cell_exit` - Cell event handlers return
- `hovered_cell` - Currently hovered cell (read-only) moves = {
mcrfpy.Key.W: (0, -1),
--- mcrfpy.Key.A: (-1, 0),
mcrfpy.Key.S: (0, 1),
*Last updated: 2025-11-29* mcrfpy.Key.D: (1, 0),
}
if key in moves:
dx, dy = moves[key]
player_move(dx, dy)
scene.on_key = handle_key
```
### Button Widget
```python
def make_button(text, x, y, callback):
btn = mcrfpy.Frame(pos=(x, y), size=(120, 40),
fill_color=mcrfpy.Color(60, 60, 80))
label = mcrfpy.Caption(text=text, x=10, y=8)
btn.children.append(label)
def on_click(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
callback()
btn.on_click = on_click
btn.on_enter = lambda pos: setattr(btn, 'fill_color', mcrfpy.Color(80, 80, 110))
btn.on_exit = lambda pos: setattr(btn, 'fill_color', mcrfpy.Color(60, 60, 80))
return btn
```
### Scene Switching with Keys
```python
game_scene = mcrfpy.Scene("game")
menu_scene = mcrfpy.Scene("menu")
def game_keys(key, action):
if key == mcrfpy.Key.ESCAPE and action == mcrfpy.InputState.PRESSED:
menu_scene.activate()
def menu_keys(key, action):
if key == mcrfpy.Key.ESCAPE and action == mcrfpy.InputState.PRESSED:
game_scene.activate()
game_scene.on_key = game_keys
menu_scene.on_key = menu_keys
```
---
## Testing Input
Use the automation API to simulate input in tests:
```python
from mcrfpy import automation
# Keyboard
automation.keyDown("w") # Press W
automation.keyUp("w") # Release W
automation.press("space") # Press and release
# Mouse
automation.click(100, 200, button='left')
automation.click(100, 200, button='right')
automation.moveTo(300, 400) # Move mouse
# Screenshots for verification
automation.screenshot("test_result.png")
```
See [[Writing-Tests]] for complete testing patterns.
---
## API Quick Reference
**Scene:**
- `scene.on_key = handler` - Register keyboard handler `(key: Key, action: InputState)`
**UIDrawable (Frame, Caption, Sprite, Grid, etc.):**
- `on_click` - Click handler: `(pos: Vector, button: MouseButton, action: InputState)`
- `on_enter` - Mouse enter: `(pos: Vector)`
- `on_exit` - Mouse leave: `(pos: Vector)`
- `on_move` - Mouse move: `(pos: Vector)`
- `hovered` - Read-only bool
**Grid cell events:**
- `on_cell_click` - Cell click: `(cell_pos: Vector, button: MouseButton, action: InputState)`
- `on_cell_enter` - Cell enter: `(cell_pos: Vector)`
- `on_cell_exit` - Cell leave: `(cell_pos: Vector)`
- `hovered_cell` - Read-only `(x, y)` tuple or `None`
**Enums:**
- `mcrfpy.Key` - Keyboard key codes (A-Z, Num0-9, F1-F15, arrows, modifiers)
- `mcrfpy.MouseButton` - LEFT, RIGHT, MIDDLE, SCROLL_UP, SCROLL_DOWN
- `mcrfpy.InputState` - PRESSED, RELEASED
**Deprecated (still functional but not recommended):**
- `mcrfpy.keypressScene(callback)` - Use `scene.on_key` instead
- Old callback signature `(key: str, pressed: bool)` - Use enum-based signature instead
---
*Last updated: 2026-02-07*