From 612cd2c1019ca040692016ba5ddccfe2400cc987 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 25 Oct 2025 22:44:42 +0000 Subject: [PATCH] Add "Input-and-Events" --- Input-and-Events.-.md | 395 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 Input-and-Events.-.md diff --git a/Input-and-Events.-.md b/Input-and-Events.-.md new file mode 100644 index 0000000..30cec4f --- /dev/null +++ b/Input-and-Events.-.md @@ -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