Add "Input-and-Events"

John McCardle 2025-10-25 22:44:42 +00:00
parent dab5590077
commit 612cd2c101
1 changed files with 395 additions and 0 deletions

395
Input-and-Events.-.md Normal file

@ -0,0 +1,395 @@
# Input and Events
## Overview
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:**
- [[UI-Component-Hierarchy]] - UI element interaction
- [[Entity-Management]] - Entity-based input (click handlers)
- [[Writing-Tests]] - Testing input with automation API
**Key Files:**
- `src/GameEngine.cpp::processEvent()` - Event dispatch
- `src/Scene.cpp::input()` - Scene input handling
- `src/McRFPy_API.cpp` - Python callback registration
**Related Issues:**
- [#128](../../issues/128) - Middle-click entity selection
- [#1](../../issues/1) - Window resize events
---
## Keyboard Input
### Scene-Level Keyboard Callbacks
Register a callback that fires on every key press/release:
```python
import mcrfpy
def handle_key(key: str, pressed: bool):
"""
key: Key name (e.g., "W", "Space", "Escape", "Up")
pressed: True on press, False on release
"""
if key == "Escape" and pressed:
print("Escape pressed!")
mcrfpy.setScene("menu")
if key == "W":
if pressed:
player.start_moving_north()
else:
player.stop_moving_north()
# Register for current scene
mcrfpy.keypressScene(handle_key)
```
### Key Names
Common key names from SFML:
- Letters: `"A"`, `"B"`, `"C"`, ... `"Z"`
- Numbers: `"Num0"`, `"Num1"`, ... `"Num9"`
- Arrows: `"Up"`, `"Down"`, `"Left"`, `"Right"`
- Special: `"Space"`, `"Enter"`, `"Escape"`, `"Tab"`, `"LShift"`, `"RShift"`, `"LControl"`, `"RControl"`
- Function: `"F1"`, `"F2"`, ... `"F12"`
See `src/McRFPy_API.cpp::McRFPy_API_keypressScene()` for full key mapping.
---
## Mouse Input
### Click Events
Mouse clicks are dispatched to UI elements based on screen position:
```python
import mcrfpy
# Create a frame with click handler
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 50))
def on_frame_click(button: int, x: int, y: int):
"""
button: 0=left, 1=right, 2=middle
x, y: Mouse position in window coordinates
"""
print(f"Frame clicked with button {button} at ({x}, {y})")
frame.click = on_frame_click
```
### Entity Click Handlers
Entities can register click callbacks for grid-based interaction:
```python
player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=1)
def on_entity_click(button: int):
if button == 0: # Left-click
print("Selected player!")
elif button == 1: # Right-click
print("Context menu for player")
elif button == 2: # Middle-click
print("Middle-clicked entity")
player.click = on_entity_click
```
**Issue #128** adds proper middle-click support for entity selection.
### Mouse Position
Get current mouse position in game coordinates:
```python
# Window coordinates (pixels)
window_x, window_y = mcrfpy.getMousePos()
# Convert to game coordinates (respects viewport)
game_x, game_y = mcrfpy.windowToGameCoords(window_x, window_y)
```
---
## Window Events
### Resize Events
**Issue #1** tracks window resize event support. Current workaround:
```python
def update_on_resize(ms):
"""Timer-based resize detection"""
current_size = mcrfpy.getWindowSize()
if current_size != last_size:
on_window_resized(current_size)
mcrfpy.setTimer("resize_check", update_on_resize, 100)
```
### Focus Events
Window focus/unfocus events are not currently exposed to Python. The engine does not pause automatically when focus is lost.
---
## Event Priority and Propagation
### Click Dispatch Order
Clicks are dispatched in reverse render order (top-to-bottom):
1. **UI elements with highest z_index** receive clicks first
2. If a UI element handles the click, propagation stops
3. **Entities on grids** receive clicks if no UI element handled it
4. **Grid cells** receive clicks last
```python
# High z_index UI blocks clicks to entities below
overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), z_index=100)
overlay.click = lambda btn, x, y: True # Blocks all clicks
# Remove overlay to allow clicks through
ui.remove(overlay)
```
### Keyboard Priority
Keyboard events are dispatched only to the current scene's registered callback. There is no concept of "focused" UI elements for keyboard input.
---
## Common Patterns
### Modal Dialog
Capture all input while a dialog is open:
```python
class ModalDialog:
def __init__(self, message: str):
self.frame = mcrfpy.Frame(pos=(300, 200), size=(400, 200), z_index=1000)
self.caption = mcrfpy.Caption(text=message, pos=(310, 220))
self.frame.click = self._on_click
# Save previous input handlers
self.prev_keypress = mcrfpy.keypressScene(self._on_key)
ui = mcrfpy.sceneUI("game")
ui.append(self.frame)
ui.append(self.caption)
def _on_click(self, btn, x, y):
return True # Block all clicks
def _on_key(self, key, pressed):
if key == "Enter" and pressed:
self.close()
return True # Block all keys
def close(self):
ui = mcrfpy.sceneUI("game")
ui.remove(self.frame)
ui.remove(self.caption)
mcrfpy.keypressScene(self.prev_keypress) # Restore
```
### WASD Movement
Handle movement keys with press/release state:
```python
class PlayerController:
def __init__(self, entity):
self.entity = entity
self.moving = {"W": False, "A": False, "S": False, "D": False}
def handle_key(self, key, pressed):
if key in self.moving:
self.moving[key] = pressed
self._update_velocity()
def _update_velocity(self):
vx = 0
vy = 0
if self.moving["W"]: vy -= 1
if self.moving["S"]: vy += 1
if self.moving["A"]: vx -= 1
if self.moving["D"]: vx += 1
# Normalize diagonal movement
import math
magnitude = math.sqrt(vx*vx + vy*vy)
if magnitude > 0:
vx /= magnitude
vy /= magnitude
self.entity.velocity_x = vx * 5.0
self.entity.velocity_y = vy * 5.0
controller = PlayerController(player)
mcrfpy.keypressScene(controller.handle_key)
```
### Context Menu
Right-click to show context-sensitive options:
```python
def show_context_menu(entity, x, y):
"""Show menu at mouse position"""
menu_frame = mcrfpy.Frame(pos=(x, y), size=(150, 100), z_index=500)
# Menu options
option1 = mcrfpy.Caption(text="Examine", pos=(x+10, y+10))
option2 = mcrfpy.Caption(text="Attack", pos=(x+10, y+30))
option3 = mcrfpy.Caption(text="Talk", pos=(x+10, y+50))
def on_menu_click(btn, mx, my):
# Determine which option was clicked
if y+10 <= my < y+30:
entity.examine()
elif y+30 <= my < y+50:
entity.attack()
elif y+50 <= my < y+70:
entity.talk()
# Close menu
ui = mcrfpy.sceneUI("game")
ui.remove(menu_frame)
ui.remove(option1)
ui.remove(option2)
ui.remove(option3)
menu_frame.click = on_menu_click
ui = mcrfpy.sceneUI("game")
ui.append(menu_frame)
ui.append(option1)
ui.append(option2)
ui.append(option3)
# Entity right-click handler
def entity_right_click(button):
if button == 1: # Right-click
mx, my = mcrfpy.getMousePos()
show_context_menu(entity, mx, my)
entity.click = entity_right_click
```
### Hotbar / Quick Slots
Number keys for quick item access:
```python
hotbar = [None] * 10 # 10 slots
def handle_hotbar(key, pressed):
if pressed and key.startswith("Num"):
slot = int(key[3:]) # "Num1" -> 1
if 0 <= slot < 10 and hotbar[slot]:
hotbar[slot].use()
mcrfpy.keypressScene(handle_hotbar)
```
---
## Testing Input
### Automation API
Use the automation API to simulate input in tests:
```python
import mcrfpy
from mcrfpy import automation
# Simulate keyboard input
automation.keypress("W", True) # Press W
automation.keypress("W", False) # Release W
# Simulate mouse clicks
automation.click(100, 200, button=0) # Left-click at (100, 200)
automation.click(150, 250, button=1) # Right-click
# Wait for effects
import time
time.sleep(0.1)
# Verify results with screenshot
automation.screenshot("test_result.png")
```
See [[Writing-Tests]] for complete testing patterns.
---
## Performance Considerations
### Event Handler Complexity
Event handlers run on the main thread and block rendering:
```python
# BAD: Expensive computation in event handler
def handle_key(key, pressed):
if key == "Space" and pressed:
# This will freeze the game!
for i in range(1000000):
expensive_calculation()
# GOOD: Defer expensive work to timer
def handle_key(key, pressed):
if key == "Space" and pressed:
mcrfpy.setTimer("deferred_work", do_expensive_work, 10)
```
### Click Handler Optimization
Avoid creating handlers that search large collections:
```python
# BAD: O(n) search on every click
def on_click(btn, x, y):
for entity in all_entities: # Expensive!
if entity.contains_point(x, y):
entity.select()
# GOOD: Use spatial data structures (see [[Grid-System]])
def on_click(btn, x, y):
grid_x, grid_y = screen_to_grid(x, y)
entity = grid.get_entity_at(grid_x, grid_y) # O(1)
if entity:
entity.select()
```
---
## API Reference
See [`docs/api_reference_dynamic.html`](../../src/branch/master/docs/api_reference_dynamic.html) for complete input API documentation.
**Key Functions:**
- `mcrfpy.keypressScene(callback)` - Register keyboard handler
- `mcrfpy.getMousePos() -> (int, int)` - Get mouse position
- `mcrfpy.windowToGameCoords(x, y) -> (float, float)` - Convert coordinates
**UI Element Properties:**
- `element.click = callback` - Click handler for any UIDrawable
- `entity.click = callback` - Click handler for entities
---
**Navigation:**
- [[Home]] - Documentation hub
- [[Entity-Management]] - Entity interaction patterns
- [[UI-Component-Hierarchy]] - UI element click regions
- [[Writing-Tests]] - Testing input with automation API