Skip to content

feat: screen capture workflow - #349

Merged
devmobasa merged 13 commits into
mainfrom
feat/screen-capture-workflow
Aug 23, 2026
Merged

feat: screen capture workflow#349
devmobasa merged 13 commits into
mainfrom
feat/screen-capture-workflow

Conversation

@devmobasa

Copy link
Copy Markdown
Owner

Summary

  • Add a native frozen-image region picker with exact pixel geometry, keyboard controls, square selection, magnified loupe, and compositor-aware window snapping.
  • Add interactive Review with Copy, Save, Both, Board, selection moving/nudging, and an include- drawings toggle.
  • Include committed Wayscriber annotations in full-screen and region captures by default, with a live picker preview matching the exported PNG.
  • Add Measure Mode and live rectangle/ellipse size readouts.
  • Verify clipboard PNG publication with a bounded read-back transaction while retaining support for large screenshots.
  • Add themed Review controls, selection-anchored measurements, and context-sensitive cursor feedback.
  • Preserve raw crops for Board insertion so existing board annotations are not duplicated.

User-visible behavior

  • Native region captures are PNG-only.
  • Committed annotations are included by default; transient UI and unfinished strokes remain excluded.
  • Space enters window-selection mode on supported Hyprland and Sway environments.
  • Interactive Review offers Copy, Save, Both, and Board.
  • D toggles annotations for the current reviewed export.
  • Measure Mode reports logical screen dimensions without capturing anything.

Refine the interactive region Review bar, selection badge, crosshair, and cursor feedback while keeping Copy, Save, Both, Board, and annotated-export controls.
`collapsible_match` fires on clippy 1.95 (the toolchain CI pins) but not on
1.97, so `-D warnings` failed there while passing locally.

Clippy's own suggestion is a match guard, which would be wrong here: a guard
that evaluates false stops matching and falls through to the permissive
catch-all, turning a fail-closed argv gate into a no-op for exactly the
arguments it exists to reject. Name the condition instead — the arm body is no
longer a lone `if`, so the lint has nothing to collapse and the semantics are
untouched.
Review could move and nudge a rectangle but never resize one, so a crop that
was a few pixels short had to be drawn again. Four corners plus four edge
midpoints now sit on the reviewed rectangle: a corner moves two edges, a
midpoint moves one, and the opposite edges stay put.

Reuses the vocabulary already in the tree — `SelectionHandle`'s eight variants
and the resize cursors the canvas selection handles use — rather than inventing
a second one. The frame drops its corner arms in Review so they cannot fight
the corner grips for the same pixels.

Three details worth stating:

- A grip records the offset between the pointer and the edges it owns when it
  is grabbed. Grips are placed on the display rectangle, which rounds outward
  from the image rectangle, so without the offset a click with no motion would
  re-round that position and shift an edge by a pixel at a fractional scale.
- Hit testing takes the nearest grip, not the first. On a rectangle small
  enough that every grab target covers all of it, first-match would leave only
  the top-left corner reachable — growable up and left but never down and right.
- Grips claim at most a quarter of each side, so a small rectangle always keeps
  a draggable centre. Chip and grab target scale as one, so what is drawn stays
  what is grabbable.

Pointer, touch and stylus all press through `begin_region_selection`, so the
grip check sits in the shared event rather than in any one handler.
The Review toggle said "Include drawings in exports" and Board ignored it. The
original reasoning was that pasting a composited crop onto the board it came
from bakes a second copy of those annotations — true, but the toggle is the
control for exactly that, so turning it off is the answer rather than
overriding it per destination.

Board could not simply be let through: the composed path deliberately skips the
event-loop crop, so the Board arm's raw pixels would have been absent. Both
destinations now take their PNG from one `region_render_job`, so what Copy
writes and what Board pastes can only differ by the toggle — a construction
guarantee rather than a convention. `include_drawings_for_submit` is gone
instead of relaxed: once Board stopped being special it was an identity
function, a named place that no longer encoded a decision.

`RegionRenderSource::{Annotated, Raw}` replaces two independent Options, so the
impossible pairs are not representable and the mapping failure is a real arm
rather than a panic.

Board placement now maps the authoritative image rectangle instead of the
picker's outward-rounded display rectangle. Composition uses that same exact
world rectangle, so the pasted image lands where the annotations were drawn
rather than stretched by a quantization the chrome introduced; each edge rounds
once, to nearest, instead of compounding two outward roundings that could only
ever grow the crop. A rectangle whose edges collapse onto one integer still has
to be widened to a whole board pixel, which is the one documented exception to
the half-pixel bound.

A Board placement that cannot map now cancels with a toast instead of returning
silently, which would have left Review painted over a reservation nothing could
complete.
Searching the palette for "capture selection" turned up nothing, even though
the action has a default chord and a help-overlay row. Palette membership is a
curated list, and the capture entries had drifted out of step with the help
overlay's: four actions were in help but not the palette, and
`capture_full_screen` was on no surface at all — palette, help and toolbar all
false — so a bound action was unreachable unless you already knew its chord.

The rule applied here, and stated in the contract list: an action a user can
bind must be runnable without knowing its shortcut. The help overlay is left
alone; it is curated for density and is already long.
Interactive capture shipped unbound, so the feature was invisible unless a user
went looking for it, while Ctrl+Shift+C ran the one-step region copy. Interactive
is a strict superset of that: crop, then Ctrl+C in Review reaches the same
clipboard result from the same chord, and Save, Both, Board, resize and nudge
become reachable from a key people already press.

`capture_region_interactive` now defaults to Ctrl+Shift+C and
`capture_clipboard_selection` ships unbound. The cost is honest: a drag that
used to copy on release now stops in Review and needs one more keystroke.

BREAKING CHANGE: Ctrl+Shift+C opens the interactive region picker instead of
copying the selection immediately. Configuration is sparse, so anyone who wrote
`capture_clipboard_selection` explicitly keeps it — the new compiled-in default
stands down with a skipped-default notice rather than taking away a shortcut
they chose. Everyone on defaults moves. Bind `capture_clipboard_selection` to
get the one-step copy back.
Rounding the near edge first and then pushing the far edge out anchors the
forced pixel to an already-rounded position, so the two errors compound:
`[10.5, 10.75]` placed at `[11, 12]`, 1.25 past its far edge, and the bound
approaches 1.5 as the crop narrows. A collapsed axis now takes the unit cell
centred on the crop, which holds every edge strictly inside one board pixel —
the best achievable, since a one-pixel cell standing in for a sub-pixel crop
has an edge up to a pixel away by construction.

Two boundary cases failed outright before, each for its own reason: a crop
straddling `i32::MAX` overflowed `checked_add(1)` and one at `i32::MIN` rounded
its centre below the representable range, though `[MAX - 1, MAX]` and
`[MIN, MIN + 1]` were available to both. The cell start is clamped into range
instead. Clamping alone would license placing a crop that has run off the range
entirely, so both edges are then checked against the bound and the placement is
refused rather than misplaced.

The exception was also described wrongly. Being under a board pixel across is
necessary but not sufficient — position decides too, and `[0, 2/3]` rounds
normally to `[0, 1]` — so the docs now say "when both rounded edges coincide".

The sweep classified collapse by the placed width, which swept ordinary
intervals that legitimately round to width one into the loose branch and left
the tighter bound untested on the cases that must keep it. It now classifies
from the rounded edges and holds collapsed placements strictly under one pixel.
@devmobasa
devmobasa merged commit d51b191 into main Aug 23, 2026
2 checks passed
@devmobasa
devmobasa deleted the feat/screen-capture-workflow branch August 23, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant