docs: add input-and-placement OpenSpec change and update AGENTS.md

Plan input handling and pattern placement before Game of Life mechanics:
keyboard bindings config, mouse placement with preview/rotation,
HiDPI scaling, and cell grid. Update AGENTS.md project notes.
This commit is contained in:
2026-11-08 14:30:09 +01:00
parent 403bcbbfdf
commit 5e7abfb56b
12 changed files with 436 additions and 40 deletions
View File
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-08
@@ -0,0 +1,80 @@
# Design
## Context
Current application (`wlgol.c`) renders an animated stripe pattern into a fixed 400x400 shm buffer, handles only Escape, and ignores configure size and output scale. Input modules (`tkeyboard`, `tpointer`) exist but pointer motion/buttons are unimplemented and keyboard callbacks do not carry modifiers. HiDPI is unsupported. See `proposal.md` for motivation.
## Goals / Non-Goals
**Goals:**
- Configurable, modifier-aware key bindings with TOML merge over defaults.
- Interactive pattern placement with preview, rotation, click placement, cancellation.
- HiDPI-correct rendering (fractional and integer).
- Cell grid storage/rendering with resize preservation and wall boundaries.
**Non-Goals:**
- Game of Life simulation rules (future change).
- Text rendering/HUD (status via stdout).
- Persistent grid save/load.
- Fractional scaling on compositors lacking the protocol beyond integer fallback.
## Decisions
### D1: TOML config via vendored `tomlc99`
- **Choice:** Vendor `tomlc99` (single .c/.h) for parsing `~/.config/wlgol/keys.conf`.
- **Rationale:** User requested TOML; no system TOML library available; vendoring avoids new system dependency.
- **Alternative:** Hand-rolled subset parser (less code, but TOML edge cases and maintenance burden).
### D2: Binding representation
- **Choice:** Store bindings as array of `{mods_mask, keysym, action}`. Lookup by exact modifier mask (ignoring lock mods) and keysym on key press.
- **Rationale:** Simple, fast enough for single-threaded event loop.
- **Alternative:** Hash table keyed by (mods, keysym) — premature; linear scan over ~20 bindings is trivial.
### D3: Keyboard callback carries modifiers
- **Choice:** Extend `keyboard_key_handler_t` to `(..., uint32_t mods, void *user_data)`, computed in `tkeyboard.c` from xkb state.
- **Rationale:** Binding matching needs modifier state; centralizes xkb modifier queries.
### D4: HiDPI via fractional scale + viewporter
- **Choice:** Prefer `wp_fractional_scale_v1`; render physical buffer = ceil(logical * scale); set buffer_scale=1 and `wp_viewport` destination = logical size. Fallback to `wl_output.scale` integer (treat as scale value).
- **Rationale:** Exact fractional scaling; unifies rendering path through viewporter.
- **Alternative:** Integer-only scaling (simpler but blurry/wrong at 125%/150%).
### D5: Logical canvas and grid
- **Choice:** Logical surface size from xdg configure (default 400x400). Grid dimensions = logical / 8. Cell size in logical pixels fixed at 8.
- **Rationale:** Consistent interaction across scales; HiDPI keeps physical cell size proportional.
### D6: Grid storage and resize
- **Choice:** `struct grid { int cols, rows; unsigned char *cells; }` (0/1). On resize, allocate new buffer, copy overlapping region, free old.
- **Rationale:** Preserves user work on window resize; simple.
### D7: Pattern definitions and rotation
- **Choice:** Each pattern stores base cell offsets and a rotation (0..3). Transform offsets at use time; bounding box computed from transformed offsets. Anchor = top-left of bounding box at cursor cell.
- **Rationale:** Avoids duplicating 4 variants per pattern; rotation stays consistent.
### D8: Placement state machine
- **Choice:** States `IDLE` and `ARMED`. Key select → ARMED; motion updates preview cell; rotate increments rotation; left click places if in-bounds (stays ARMED); Escape → IDLE.
- **Rationale:** Matches requested UX; tool remains active for repeated placement.
### D9: Protocols generation
- **Choice:** Vendor/copy `viewporter.xml` and `fractional-scale-v1.xml` into repo (from system wayland-protocols) and generate headers/sources via wayland-scanner in CMake, mirroring xdg-shell.
- **Rationale:** Reproducible builds without depending on system protocol paths.
## Risks / Trade-offs
- [Fractional scaling complexity] → Mitigation: isolate in `scale.c`; integer fallback ensures basic functionality.
- [Resize resets grid partially] → Mitigation: preserve overlapping region; document behavior.
- [tomlc99 vendoring licensing] → Mitigation: verify license (MIT) before commit.
- [No font rendering] → Status via stdout; acceptable for this change.
- [Placement near edges] → Mitigation: reject invalid placement; preview indicates invalid state (e.g., red tint).
## Migration Plan
1. Add generated protocol files and CMake changes.
2. Vendor tomlc99.
3. Implement modules and integrate into wlgol.c.
4. Update tests (hashtable unchanged; add config/grid unit tests if feasible).
5. Run ctest and build.
## Open Questions
None — all blocking decisions resolved during exploration.
@@ -0,0 +1,34 @@
# Proposal
## Why
wlgol currently renders an animated stripe pattern and has no Game of Life state, no input handling beyond Escape, and no HiDPI awareness. Before implementing the automaton, the application needs a usable interaction layer: configurable keyboard bindings, mouse-driven placement of patterns with a live preview, and rendering that stays crisp on scaled displays. This change establishes that layer so later GoL mechanics can build on it.
## What Changes
- Add configurable keyboard bindings loaded from `~/.config/wlgol/keys.conf` (TOML), merged over built-in defaults, with modifier-aware matching (Ctrl/Shift/Alt/Super).
- Extend keyboard handling to report active modifiers and dispatch bound actions (`quit`, `cancel`, `rotate`, `select:<pattern>`, `clear`, `pause`, `step`, `speed_up`, `speed_down`, `toggle_grid`).
- Implement mouse tracking and click handling: selecting a tool shows a rotating preview that follows the cursor; `Space` rotates 90°; left click places the pattern (tool stays armed); `Esc` cancels.
- Add a cell grid (8 logical px per cell, wall boundary) with storage, resize-with-preserve, and rendering of alive cells.
- Add four built-in patterns (Glider, Blinker, Block, Toad) with rotation support.
- Add HiDPI scaling (fractional via `wp_fractional_scale` + `wp_viewporter`, fallback to `wl_output.scale`) with logical/physical coordinate conversion.
- Auto-generate a default `keys.conf` on first run if missing.
## Capabilities
### New Capabilities
- `keyboard-bindings`: configurable, modifier-aware key-to-action mapping with defaults and TOML override.
- `pattern-placement`: tool selection, live preview, rotation, click placement, cancellation.
- `display-scaling`: fractional and integer HiDPI scaling with logical/physical conversion.
- `cell-grid`: grid storage, resize handling, wall boundary, alive-cell rendering.
### Modified Capabilities
None.
## Impact
- Affected code: `wlgol.c`, `tkeyboard.c/h`, `tpointer.c/h`; new modules `config`, `grid`, `pattern`, `scale`, `render`.
- New dependency: vendored `tomlc99` for TOML parsing; generated Wayland protocols `viewporter` and `fractional-scale-v1`.
- Config I/O under `~/.config/wlgol/`; no public API changes.
@@ -0,0 +1,62 @@
# Spec Delta
## Purpose
Stores and renders the Game of Life cell grid with a fixed cell size and wall boundaries, independent of future simulation rules.
## ADDED Requirements
### Requirement: Grid stores alive and dead cells
The system SHALL maintain a grid of cells, each either alive or dead, addressable by column and row.
#### Scenario: Cell access
- **WHEN** a cell is set to alive
- **THEN** reading that cell returns alive
- **AND** cells never set remain dead
### Requirement: Boundary cells are treated as dead
The system SHALL treat any cell outside the grid bounds as dead and shall not store state outside the grid.
#### Scenario: Out of bounds read
- **WHEN** a cell position outside the grid is queried
- **THEN** the result is dead
### Requirement: Grid resizes while preserving overlapping cells
The system SHALL preserve cells that remain within bounds when the grid dimensions change, clearing only cells that fall outside the new bounds.
#### Scenario: Enlarge grid
- **WHEN** the grid grows in either dimension
- **THEN** previously alive cells within the old bounds remain alive
#### Scenario: Shrink grid
- **WHEN** the grid shrinks in either dimension
- **THEN** cells outside the new bounds are discarded
- **AND** remaining cells keep their state
### Requirement: Cell size is fixed in logical pixels
The system SHALL use a cell size of 8 logical pixels, so grid dimensions derive from the logical surface size.
#### Scenario: Dimension calculation
- **WHEN** the logical surface is 400 pixels wide
- **THEN** the grid has 50 columns
### Requirement: Alive cells are rendered
The system SHALL render alive cells distinctly from dead cells.
#### Scenario: Render alive cell
- **WHEN** a cell is alive
- **THEN** it is drawn with the alive cell color
### Requirement: Grid can be cleared
The system SHALL set all cells to dead when the clear action is triggered.
#### Scenario: Clear grid
- **WHEN** the user triggers the clear action
- **THEN** all cells become dead
### Requirement: Grid lines can be toggled
The system SHALL show or hide grid lines when the toggle_grid action is triggered.
#### Scenario: Toggle lines
- **WHEN** the user triggers the toggle_grid action
- **THEN** grid line visibility flips
@@ -0,0 +1,45 @@
# Spec Delta
## Purpose
Keeps the application rendering crisp on displays with integer or fractional scaling by converting between logical and physical coordinates.
## ADDED Requirements
### Requirement: Fractional scale is applied when available
The system SHALL use the compositor-provided fractional preferred scale for rendering when the fractional scale protocol is available.
#### Scenario: Fractional display
- **WHEN** the compositor sends a preferred scale of 1.5
- **THEN** the render buffer is created at 1.5 times the logical size
- **AND** the buffer is displayed at the logical size
### Requirement: Integer scale is used as fallback
The system SHALL use the output's integer scale when fractional scale is unavailable.
#### Scenario: No fractional protocol
- **WHEN** the compositor does not support fractional scaling
- **THEN** the render buffer uses the output integer scale
- **AND** the buffer is displayed at the logical size
### Requirement: Input coordinates remain logical
The system SHALL report mouse positions and grid cells in logical coordinates regardless of the active scale.
#### Scenario: Scaled click
- **WHEN** the user clicks on a scaled display
- **THEN** the selected grid cell matches the visual location of the cursor
### Requirement: Rendering uses physical resolution
The system SHALL render all content at physical resolution so that lines and cells stay sharp at the active scale.
#### Scenario: Sharp cells
- **WHEN** the application runs on a 2x scaled display
- **THEN** cells are drawn with 2 physical pixels per logical pixel
### Requirement: Scale changes trigger re-render
The system SHALL recreate buffers and re-render when the active scale changes.
#### Scenario: Scale change
- **WHEN** the compositor changes the preferred scale
- **THEN** the application updates its buffers
- **AND** subsequent frames use the new scale
@@ -0,0 +1,60 @@
# Spec Delta
## Purpose
Allows users to configure keyboard shortcuts that map key combinations to application actions, with sensible built-in defaults when no configuration file exists.
## ADDED Requirements
### Requirement: Default bindings are available without configuration
The system SHALL provide built-in default bindings for all documented actions, including digits 1-4 selecting the Glider, Blinker, Block, and Toad patterns, Space rotating, Escape cancelling, and Ctrl+Q quitting.
#### Scenario: No config file present
- **WHEN** the application starts and `~/.config/wlgol/keys.conf` does not exist
- **THEN** the application uses the built-in default bindings
- **AND** creates the config file with the default contents
### Requirement: Configuration file overrides defaults by merge
The system SHALL read `~/.config/wlgol/keys.conf` as TOML when it exists and merge its entries over the built-in defaults, so only specified keys change.
#### Scenario: Partial override
- **WHEN** the config file defines a binding for key "1" only
- **THEN** key "1" uses the configured action
- **AND** all other keys keep their default bindings
#### Scenario: Full replacement of a key
- **WHEN** the config file defines a binding for a key that has a default
- **THEN** the configured action replaces the default for that key
### Requirement: Key combinations are modifier-aware
The system SHALL match a binding only when the modifiers required by the binding are active, supporting Ctrl, Shift, Alt, and Super.
#### Scenario: Modifier required
- **WHEN** the user presses Q while Ctrl is held and a `ctrl+q = quit` binding exists
- **THEN** the quit action is triggered
#### Scenario: Modifier not held
- **WHEN** the user presses Q without Ctrl while a `ctrl+q = quit` binding exists
- **THEN** the quit action is not triggered by that binding
### Requirement: Readable key aliases are supported
The system SHALL accept human-readable key names such as space, escape, enter, and arrow names in binding keys.
#### Scenario: Alias binding
- **WHEN** the config file contains `space = rotate`
- **THEN** pressing the space bar triggers the rotate action
### Requirement: Malformed entries are ignored
The system SHALL ignore malformed or unrecognized binding entries and continue using defaults for the affected keys.
#### Scenario: Invalid entry
- **WHEN** the config file contains an entry with an unknown action
- **THEN** that entry is ignored
- **AND** the application still starts successfully
### Requirement: Actions trigger on key press
The system SHALL dispatch a bound action when the corresponding key is pressed, not on release.
#### Scenario: Press triggers action
- **WHEN** the user presses a key with a valid binding
- **THEN** the bound action is invoked once for that press
@@ -0,0 +1,61 @@
# Spec Delta
## Purpose
Lets users select a Game of Life pattern, preview it under the mouse cursor, rotate it, and place copies on the grid with a click.
## ADDED Requirements
### Requirement: Selecting a pattern arms a placement tool
The system SHALL activate a placement tool for the selected pattern when a `select:<pattern>` action is triggered, and show a preview following the cursor.
#### Scenario: Tool activation
- **WHEN** the user triggers `select:glider`
- **THEN** a glider preview appears at the current cursor position
- **AND** the placement tool remains active
### Requirement: Preview follows cursor motion
The system SHALL update the preview position to track the current mouse cursor cell while a tool is active.
#### Scenario: Mouse moves
- **WHEN** the user moves the mouse while a tool is active
- **THEN** the preview moves to the cell under the cursor
### Requirement: Tool can be rotated in 90-degree steps
The system SHALL rotate the active preview by 90 degrees each time the rotate action is triggered.
#### Scenario: Rotate preview
- **WHEN** the user triggers the rotate action while a tool is active
- **THEN** the preview rotates 90 degrees clockwise
- **AND** remains at the same anchor cell
### Requirement: Left click places the pattern when valid
The system SHALL write the active pattern's alive cells into the grid at the preview position when the user left-clicks and the pattern fits entirely within the grid.
#### Scenario: Valid placement
- **WHEN** the user left-clicks while a tool is active and the preview fits in the grid
- **THEN** the pattern's cells become alive in the grid
- **AND** the tool remains active for further placement
### Requirement: Invalid placement is rejected
The system SHALL not modify the grid when a left click occurs while the preview extends outside the grid.
#### Scenario: Out of bounds
- **WHEN** the user left-clicks while the preview extends outside the grid
- **THEN** no cells are changed
- **AND** the tool remains active
### Requirement: Escape cancels the active tool
The system SHALL deactivate the placement tool and remove the preview when the cancel action is triggered.
#### Scenario: Cancel placement
- **WHEN** the user triggers the cancel action while a tool is active
- **THEN** the preview disappears
- **AND** no further clicks place a pattern until a tool is selected again
### Requirement: Click without active tool does nothing
The system SHALL ignore left clicks when no placement tool is active.
#### Scenario: No tool
- **WHEN** the user left-clicks while no tool is active
- **THEN** the grid is unchanged
@@ -0,0 +1,47 @@
# Tasks
## 1. Protocol generation and build setup
- [ ] 1.1 Vendor `viewporter.xml` and `fractional-scale-v1.xml` into the repo and add wayland-scanner custom commands in `CMakeLists.txt`, verifying headers generate under `build/include`.
- [ ] 1.2 Add `tomlc99.c`/`tomlc99.h` to the source tree and CMake sources, verifying the project still configures and builds.
## 2. Configuration module
- [ ] 2.1 Implement `config.c/h` to load or auto-create `~/.config/wlgol/keys.conf`, parse TOML bindings, and merge with built-in defaults, verifying a sample file round-trips.
- [ ] 2.2 Implement binding string parsing (modifiers + key aliases) and lookup by modifier mask and keysym, verifying unit tests cover merge and modifier matching.
## 3. Keyboard module
- [ ] 3.1 Extend `keyboard_key_handler_t` to pass a modifier mask and update `tkeyboard.c` to compute it from xkb state, verifying compilation of all callers.
- [ ] 3.2 Wire keyboard events to config-based binding lookup and action dispatch, verifying Escape and Ctrl+Q trigger their actions.
## 4. Pointer module
- [ ] 4.1 Track logical cursor position in `tpointer.c/h` from motion events, verifying position updates on move.
- [ ] 4.2 Dispatch left-click events to the placement controller, verifying clicks are received.
## 5. Grid module
- [ ] 5.1 Implement `grid.c/h` with storage, bounds checks, resize-with-preserve, clear, and toggle state, verifying unit tests for resize and bounds.
- [ ] 5.2 Expose grid dimensions derived from logical surface size at 8 px per cell, verifying dimensions update on resize.
## 6. Pattern module
- [ ] 6.1 Implement `pattern.c/h` with Glider, Blinker, Block, and Toad offsets and 90-degree rotation transform, verifying rotated offsets and bounds.
- [ ] 6.2 Implement placement validity check (pattern fits within grid), verifying in-bounds and out-of-bounds cases.
## 7. Scaling module
- [ ] 7.1 Implement `scale.c/h` to bind fractional scale and viewporter, compute physical buffer size, and convert coordinates, verifying scale changes update buffer size.
- [ ] 7.2 Implement integer-scale fallback via `wl_output`, verifying fallback path builds without fractional protocol.
## 8. Rendering and application integration
- [ ] 8.1 Refactor `wlgol.c` to use logical surface size, create scaled shm buffers, and render grid plus alive cells, verifying build succeeds.
- [ ] 8.2 Add placement controller state machine (IDLE/ARMED), preview rendering, rotation, click placement, and cancellation, verifying behavior by manual run.
- [ ] 8.3 Integrate config loading, default file creation, and action dispatch into startup, verifying default config is generated.
## 9. Verification
- [ ] 9.1 Build with CMake/Ninja and run `ctest --test-dir build`, verifying all tests pass.
- [ ] 9.2 Run `./build/wlgol` under a Wayland compositor and manually verify placement, rotation, HiDPI scaling, and Esc cancellation.