# Play Grid Magnifier

**Status**: Draft | Implemented | 2026

**Author**: Toluwaleke Ogundipe

**Reviewer**: Jonathan Blandford

## Goals

An interactive magnifier on `PlayGrid` that provides a zoomed view of the grid.

## Prior Art

https://codeberg.org/haydn/typesetter/src/branch/main/screenshots/light-magnifier.png

## Overall Approach

The magnifier is an overlay on the `PlayGrid` widget.

### Behaviour

The magnifier is a non-intrusive overlay that responds to standard mouse inputs
while ensuring core puzzle interactions remain uninterrupted.

- **Activation & Deactivation:** The magnifier is toggled on and off using a
  secondary (right) click anywhere on the grid. Upon activation, the lens
  immediately appears, centered on the cursor's current position.

- **Cursor Tracking:** While the magnifier is enabled, any mouse movement within
  the grid boundaries causes the lens to move with its center always at the
  current cursor position. Also, the exact point on the grid underneath the
  cursor is always at the center of the magnified projection.

- **Auto-Hiding:** If the cursor leaves the allocated area of the widget,
  the magnifier is temporarily hidden. The moment the cursor re-enters the
  widget bounds, the magnifier instantly reappears and resumes tracking.

- **Interoperability with Gameplay:** The magnifier overlay does not intercept
  standard gameplay input. A primary (left) click while the magnifier is active
  passes straight "through the lens", selecting the underlying cell exactly as
  it normally would without the magnifier. Keyboard input alike.

### State Management

`PlayGrid`'s internal state is expanded to track the magnifier's state:

- **Lifecycle:** A boolean toggle to indicate/determine whether the magnifier
  is enabled or disabled.

- **Visibility:** A separate boolean to prevent the magnifier from rendering,
  for instance, when it's enabled but the cursor has left the widget's bounds.

- **Position:** The sub-pixel coordinates of the cursor relative to the
  widget's origin are stored and continuously updated to position the magnifier.

### Event Handling

Input handling relies on dedicated GTK event controllers:

- **Toggle:** A click gesture for the secondary (right) click acts exclusively
  as the magnifier's enable/disable toggle.

- **Dynamic Motion Tracking:** Tracking mouse coordinates is computationally
  expensive and unnecessary while the magnifier is disabled. Therefore, a
  motion controller is created and attached to the widget only when the
  magnifier is toggled on, and immediately removed and destroyed when toggled off.

- **Redraw Triggers:** Any change in state (be it a toggle, cursor movement,
  or the cursor entering/leaving the widget bounds) queues a full widget redraw.

### Geometry

The radius of the target region (and in turn, the viewport) is dynamically
calculated based on the current layout config to ensure it consistently covers
an area of 3 cells across and down, regardless of the grid's zoom level.

When the center of the target region coincides with the center of a cell,
the geometry is thus:

![Magnifier Target Region Geometry](magnifier-geometry.png)

where:

- *a* represents the distance from the center point to the far outer edge of
  the adjacent cells, accounting for the center half-cell, a full cell, and
  necessary borders, i.e:

  ```
  a = cell_size / 2.0 + border_size + cell_size + border_size
  ```

- *b* represents the orthogonal distance from the center point to the immediate
  edge of the center cell (plus its border) i.e:

  ```
  b = cell_size / 2.0 + border_size
  ```

- *r* represents the radius of the target region determined by finding the
  hypotenuse of the right-angled triangle formed by *a* and *b*, ensuring the
  circle passes through the absolute corners of all four immediately adjacent
  cells. This is achieved using the Pythagorean theorem:

  ```
  r = sqrt (a * a + b * b)
  ```

The radius of the viewport is twice that of the target region, given the
magnification factor of 2.0.

### Rendering

After rendering grid overlays (enumerations, etc), the magnifier is rendered if
enabled and not hidden.

The viewport is a circle with radius as described earlier and centered at the
cursor's coordinates. It is filled with a solid background color (to prevent
the underlying grid from bleeding through magnified NULL cells) and outlined
(i.e stroked on the **outside**) with a solid color and thickness of
`border_size` to visually separate the magnified grid from the main grid.

Then, the magnified grid is rendered at a scale of 2.0 and positioned such that
the same point at which the cursor is on the grid falls right at the center of
the circle, and is clipped by the same circle.

## Open Questions

## Areas For Improvement

- [ ] Snap to the grid (not sure this will work well with the magnifier
      directly underneath the cursor).
- [ ] React to keyboard navigation by snapping to the focused cell.
      Once the cursor moves again, it should snap back to the cursor and
      continue to follow it.
- [ ] Animation when snapping between cells / the cursor.
- [ ] Variable zoom:

      - when the magnifier is enabled, the normal grid zoom controls adjust the
        magnifier's magnification factor instead
      - `magnification_factor` is the scale factor of the magnified view to the grid
      - `MIN_MAGNIFICATION_FACTOR` > 1.0
      - `BASE_MAGNIFICATION_FACTOR` = 2.0
      - `base_radius` is as computated earlier
      - `lens_radius = base_radius * BASE_MAGNIFICATION_FACTOR`
      - `target_radius = base_radius * BASE_MAGNIFICATION_FACTOR / magnification_factor`
