Skip to content

Electron: sharper drag previews and a faster 1:1 view after an edit - #26

Merged
marrasen merged 17 commits into
mainfrom
electron-edit-speed
Oct 22, 2026
Merged

marrasen merged 17 commits into
mainfrom
electron-edit-speed

Conversation

@marrasen

@marrasen marrasen commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

The Electron app gets the editing-speed work: sharper drag previews, the photo held open in Develop, and the 1:1 render after your own edit starting almost at once. Stacked on server PR #24.

What changes

  • Sharper drag previews. Drag frames go through Edits.PreviewDraft at what the photo covers in the loupe at fit, in device pixels, from 1024 to 2048. They were a fixed 1024. The cap is 2048, so letting go never softens the frame. The size follows device pixel ratio changes (useDevicePixelRatio).
  • Preset hovers never move the editor's state. A hovered look's sharp frame goes through Edits.PreviewExact. It is exact like the settle, but it doesn't tell the server the editor is on that state.
  • Develop holds the loupe's photo open with Edits.OpenInEditor (client/src/lib/editorHold.ts).
    • The hold starts after a 1 s dwell, at any selection size, and leaving the photo aborts it.
    • After a connection error it holds again on reconnect. Other errors retry after 5 s, 30 s and 30 s, then stop.
  • The 1:1 render after your own save starts after 120 ms, not 350 ms.
    • Saves, pastes and resets now return what they saved, so the edit session knows the hash of your newest real save.
    • The loupe passes immediate to useTilesWarm when no save is out and the photo shows that hash.
    • Skimming keeps the 350 ms dwell, and a burst of saves still coalesces.
  • Edit patches apply in save order. The photo list keeps the highest save number applied per photo, and skips the edit fields of older patches.
  • DEVELOPER.md and an Unreleased CHANGELOG.md section describe the change.

Tests

  • npm run typecheck, npm run lint (oxlint) and npx vitest run pass: 164 tests.
  • Wire replay against a backend from Keep the full-size decode so a 1:1 render after an edit skips the demosaic #24 on a 42 MP ARW:
    • drafts at 1792 px took 119–143 ms;
    • the settle took 172 ms;
    • the 1:1 tile after the commit took 2.2 s, from the kept decode.
  • An early scripted Electron run confirmed the hold and the kept-decode 1:1 render. Later review fixes were checked by unit tests only, not in the real app.
  • scripts/ui-verify.mjs stops at timeout: clipboard on this box, as it does on the base branch.

7 review rounds; the last one found nothing new.

🤖 Generated with Claude Code

Marcus Johansson and others added 16 commits October 20, 2026 19:45
The Electron client now uses the backend's editing-speed calls.

- Drag frames render through PreviewDraft at the size the photo covers
  in the loupe at fit, in device pixels, between 1024 and 2560. They were
  a fixed 1024, too soft to judge an edit by on a large screen.
- A hovered preset's sharp frame is a PreviewDraft at 2048, never a
  PreviewEdit settle. The backend reads every settle as "the editor is now
  on this state" and would move its kept decode to the hovered look.
- The Develop panel holds its photo open with OpenInEditor after a 350 ms
  dwell, so skimming starts no hold per step. A hold that ends with an
  error is made again after 1 s, 5 s, 5 s, then every 30 s.
- After an edit of the held photo, its 1:1 render starts at once instead
  of after the skimming dwell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The 1:1 render after the user's own save now really starts without the
  skimming dwell. The edit session records the hash each save produced,
  and the loupe passes "immediate" to useTilesWarm whenever tiles come to
  be wanted for that state. tiles.ts no longer reads editor state, so the
  guest page does not pull it in.
- Drag frames stop at 2048, the settle's size, so letting go of a slider
  never softens the photo.
- A hovered preset's sharp frame is a PreviewExact: the same pixels the
  apply gives, without telling the backend the editor is on it. Its key
  carries a hover flag, so a second settle request during one hover is
  skipped and the apply after it still settles.
- The photo is held open after a 1 s dwell, not 350 ms, so stepping
  through photos in Develop does not start a decode per step.
- Develop holds the photo its loupe shows, however many are selected.
- A hold lost with the connection is made again as soon as the client is
  connected. Backend errors are tried again after 5, 30 and 30 s, then
  given up with one warning.
- Drag frames follow device pixel ratio changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SetEditParams now returns the save's hash and number together. The edit
session takes the hash from it, as before, for the 1:1 render that
starts without a pause after the user's own save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ives

The signal for skipping the skimming dwell was the hash SetEditParams
returned. It came too late: the patch with the new hash reaches the
loupe before the answer. Saves with several photos selected never set
it, and it outlived a switch to another photo.

- Each save of the session's photo, at any selection size, now records
  the hash it was made over. Once the shown photo's hash moves past it,
  the 1:1 render skips the dwell. A photo switch, or warm tiles, forget
  it. The returned hash is no longer used.
- useTilesWarm cuts a dwell already under way short when `immediate`
  turns true, instead of reading it only as the dwell starts.
- Tests: a dwell cut short, a multi-photo save, forgetting on a photo
  switch and once warm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts tiles

Round 3 of review found two gaps in the 1:1 render after the user's own
save:

- The loupe read the last state's tile warmth in the render the next
  hash arrived in, and so forgot the save before its tiles existed. The
  warmth useTilesWarm reports is now for the tile set it was found for,
  and the save is forgotten only when its own result's tiles are warm.
- Holding undo or stepping the history started and aborted a full render
  per step. Saves of the photo are now numbered and counted while out.
  The dwell is skipped only once no save is out and the photo shows the
  newest save's result. That path still waits 120 ms, so a burst of saves
  makes one render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server numbers edit saves in the order they are written, but aprot
delivers each subscriber's patches on its own goroutine, so two saves of
one photo can arrive in reverse. The grid kept whichever came last and
could end on the overwritten edit. The photo list now remembers the
highest save number applied per photo and skips the edit fields (hash,
rotation, crop) of an older one. Rating, flag and the other fields of
that patch still apply, and patches with no number apply as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Saves of the session's photo take numbers from one counter that never
  restarts. A save sent before a photo switch can no longer answer into
  the record of a save made after coming back.
- The record keeps the hash the photo showed before its first save, so a
  save that changes nothing (crop entered and left) no longer hides the
  real edit before it.
- The user's own Reset of the session's photo is counted like any save.
  Its hash is not returned, so whether the photo moved on decides.
- Tile warmth is forgotten on leaving a state, and when a probe finds a
  once-warm tile set gone, so evicted tiles read cold again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The record keeps, for the newest save that changed something, the hash
  the loupe showed when it was sent and the hash it saved. A paste or a
  Reset, whose result is unknown, counts once the photo has moved past
  the hash it was sent from. So a paste answered before its patch no
  longer takes the overwritten earlier hash for its result, and a Reset
  back to the photo's original hash counts.
- A save that changes nothing still leaves the real edit before it.
- Tile warmth is kept when only `kick` changes (a guest pinching past 2x,
  cull fit to 1:1 on a HiDPI screen), so tiles on screen stay up.
- photoPatchReducer is documented as reading and raising the edit-save
  numbers it has seen, and the tests reset them, so test order can't
  matter. A full reload of the list is taken as is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paste and reset now return what they saved, like a single save does, so
the edit session no longer guesses a paste's or a reset's result from the
hash it was sent from.

- The record keeps the hash the newest save that changed something saved.
  A save that changes nothing, or fails, leaves the result before it, so
  a failed save no longer hides the edit still on screen.
- Paste and reset take the session photo's entry from their answer.
- The 1:1 render skips the long dwell when no save is out and the photo
  shows that hash. The unknown-result rule is gone.
- useTilesWarm records what the render asked for in a layout effect, not
  during render, so a render React throws away can't reach its cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marrasen
marrasen merged commit 12059a7 into main Oct 22, 2026
1 check passed
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