Skip to content

feat(viewer): show the edited image in a second window, with zoom and pan - #1949

Merged
CyberTimon merged 5 commits into
CyberTimon:mainfrom
lalibertemarc:feat/second-window-viewer
Oct 28, 2026
Merged

CyberTimon merged 5 commits into
CyberTimon:mainfrom
lalibertemarc:feat/second-window-viewer

Conversation

@lalibertemarc

@lalibertemarc lalibertemarc commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds a second window that shows the image being edited, for multi-monitor setups (requested on Discord). The viewer has its own zoom and pan, independent of the editor. When zoomed in, it renders at higher resolution, so it stays sharp up to 100%.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Performance improvement
  • Code refactoring
  • Documentation update
  • UI/UX improvement
  • Build/CI or Dependency update

Changes Made

Opening the viewer

Either of these opens it. Both are hidden on Android.

  • "Open in second window" in the editor's right-click menu
  • Shift+F, a new rebindable action in the View keybind section (the key was unused)

There is no toolbar button, per maintainer feedback.

Viewer window

  • Native window decorations.
  • It opens on a monitor other than the editor's when there is one.
  • It remembers its monitor, size and fullscreen state in viewer_window_state.json.
  • F11 toggles fullscreen; Esc exits it.
  • Closing the main window also closes the viewer, so the app still quits.

Zoom and pan

  • Zoom goes from fit up to 100% (1 screen pixel = 1 image pixel).
  • The viewer follows the Mouse Controls settings exactly like the editor canvas:
    • Zoom Speed Multiplier scales wheel and pinch zoom.
    • Mouse mode: the wheel zooms around the cursor; Shift+wheel pans horizontally, Alt+wheel vertically.
    • Touchpad mode: two-finger scroll pans, pinch zooms (with the editor's touchpad speed boost).
    • Left or middle drag pans.
    • A single click toggles zoom: to 100% when "Zoom photo to 100%" is on, otherwise to 2× (or the last click-zoom level). Clicking again returns to fit.
  • F11 toggles fullscreen; Esc exits it.
  • Switching to another image keeps the viewer's zoom and position, like the editor canvas. It is re-clamped to the new image's size.
  • Before/after: the toggle compare mode (B) is mirrored in the viewer. The split and side-by-side modes stay in the editor; the viewer keeps showing the edited image, and the editor's original-layer renders are never sent to it.

Rendering

  • wgpu renderer (Windows/macOS default): the viewer gets its own wgpu surface. Each frame is copied on the GPU into a texture the viewer owns. Viewer renders never write the texture that the main window and the white-balance picker read.
  • Live updates: editor frames update the viewer live while sliders are dragged. Interactive frames rendered for a zoomed-in editor region (ROI) are skipped; the viewer updates when the slider is released.
  • Sharp zoom:
    • When the visible area needs more pixels than the editor rendered, a viewer-only render is queued on the preview worker. This happens about 250 ms after zooming stops.
    • Editor jobs always run first.
    • The viewer has its own preview and GPU image cache slots, so it doesn't evict the editor's caches.
    • A lower-resolution frame of the same edit doesn't replace a sharper viewer render.
    • At fit, frames within 80–200% of the needed resolution are used as is, so the viewer doesn't re-render after every edit.
  • No wgpu renderer (Linux, or renderer turned off): the same frames and renders are sent as JPEG and placed with a CSS transform.

Shared-code changes

  • Display creation moved out of get_or_init_gpu_context into build_wgpu_display. The main window and the viewer both use it.
  • WindowEvent::Resized is now routed per window. Before, resizing any window reconfigured the main surface.
  • Window-state file read/write (read_window_state / write_window_state) is shared between the main window and the viewer.
  • AppSettings::wgpu_renderer_enabled() replaces the duplicated cfg logic.
  • The preview-resolution formula moved from useImageProcessing to calculateTargetResolution (src/utils/previewResolution.ts). The main editor calls it with the same inputs as before.
  • clear_preview_slot replaces duplicated cache resets in load_image and clear_image_caches.
  • The editor's wheel-gesture rules (zoom vs. pan per input mode and modifiers), click-zoom target and click slop moved to src/utils/canvasControls.ts (interpretWheel, clickZoomTarget, movedBeyondClick). The editor behaves as before, and the viewer uses the same code, so both canvases respond to the mouse settings identically.

Other

  • New capability viewer.json for the viewer window label.
  • Strings added to all 16 locales.

Screenshots/Videos

Context menu (please disregard the Edit in Topaz, only present on my end):
image

RapidRAW_tZxTxTHIz1 20261017_154045

Testing

  • These changes were tested locally by a human and confirmed to work.
  • I haven't added any automated tests to the code because the codebase currently lacks a test suite.

Tested with the wgpu renderer on Windows:

  • opening and closing from all three entry points
  • resizing and drag-resizing the viewer
  • live slider updates
  • zoom, pan, and sharpening when zoomed in

@enrperes also tested it on macOS (see the comment below), with no issues found. The Linux / renderer-off JPEG path has not been tested by a human.

Test Configuration:

  • OS: Windows 10 Home (19045)
  • Hardware: AMD Ryzen 5 3600X, NVIDIA GeForce RTX 2070

Checklist

  • My code follows the project's code style
  • I haven't added unnecessary AI-generated code comments
  • My changes generate no new warnings or errors

Additional Notes

Costs and limits:

  • GPU memory: at 100% on a 24MP image, the viewer uses roughly 300–500 MB of extra VRAM: its own input texture, its frame texture, and possibly a larger processor. It is freed when the viewer closes. Nothing extra is allocated while the viewer is closed.
  • Render time: a full-resolution viewer render takes about 0.3–1.5 s and can't be interrupted. If a slider drag starts during one, its first frame waits for it.
  • Geometry and effects edits: the effects/transform caches are still single-entry. These edits rebuild once per cache slot. Colour-only edits stay GPU-only in both slots.
  • macOS: the viewer surface is created on the main thread, which NSView requires.

AI Disclaimer:

Please state the involvement of AI in this PR:

  • This PR is created by an AI agent
  • This PR is mostly AI-generated but edited/merged together by a human
  • This PR was handwritten with AI assistance (spell check, logic suggestions, error resolving)
  • This PR contains only blood, sweat, and coffee (AI-free)

🤖 Generated with Claude Code

@enrperes

enrperes commented Oct 17, 2026 •

Copy link
Copy Markdown

I tested this on macOS and it works great! I haven't encountered any issues.

lalibertemarc added a commit to lalibertemarc/RapidRAW that referenced this pull request Oct 19, 2026
Bring personal in line with feat/second-window-viewer as opened in
CyberTimon#1949: shared wheelZoomFactor (the viewer now honours
the trackpad input mode), PreviewTarget::ALL, a single small preview
size, one viewer delivery path in process_preview_job, and the remaining
800x600 main window minimums using MAIN_WINDOW_MIN_SIZE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@lalibertemarc
lalibertemarc force-pushed the feat/second-window-viewer branch 4 times, most recently from f7e4a09 to db6793f Compare October 24, 2026 13:55
lalibertemarc and others added 4 commits October 28, 2026 14:05
Open a separate viewer window (toolbar button, Shift+F, editor context
menu) that shows the whole edited image fitted to the window, for
multi-monitor setups.

With the wgpu renderer the viewer gets its own surface that samples the
same processed texture as the main window, so it updates live while
dragging (ROI frames from a zoomed-in editor are skipped and the viewer
refreshes on release). Without it (Linux, or renderer off) the final
preview JPEG is forwarded on each render.

Display creation is extracted into build_wgpu_display, resize handling
is routed per window (resizing another window no longer reconfigures
the main surface), and window-state read/write is shared with the
viewer, which remembers its monitor, size and fullscreen state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The viewer window now zooms with the wheel (anchored at the cursor),
pans by dragging, and toggles fit / 100% on double-click; fullscreen
stays on F11. Switching image resets it to fit.

The viewer owns a frame texture copied from the processor, so its own
renders never touch the texture the main window and the white-balance
picker read. When the visible area needs more pixels than the editor
rendered, a viewer-only render is queued behind editor jobs on the
preview worker, using its own preview and GPU image cache slots.
Editor frames still update the viewer live while dragging; a lower
resolution frame with the same content no longer replaces a sharper
viewer render. Without the wgpu renderer the same renders are sent as
JPEG and placed with a CSS transform.

The target resolution formula and the wheel zoom factor are shared
with the main editor through calculateTargetResolution and
wheelZoomFactor, so the viewer also honours the trackpad input mode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The second window now handles input exactly like the editor canvas:
touchpad mode scrolls to pan and pinches to zoom, mouse mode pans with
Shift/Alt+wheel, middle-click drag pans, and a single click toggles
zoom, honouring "Zoom photo to 100%" (otherwise 2x or the last click
zoom). Double-click no longer zooms; F11 still toggles fullscreen.

The wheel gesture, click-zoom target and click slop move from the
editor into utils/canvasControls (replacing utils/wheelZoom) so both
canvases share one implementation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Match the main canvas: the viewer keeps its zoom and position across
image switches, re-clamped to the new image's size.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@lalibertemarc
lalibertemarc force-pushed the feat/second-window-viewer branch from 7bab328 to b8f13fe Compare October 28, 2026 18:06
Per maintainer feedback, drop the editor toolbar button. The window is
still toggled from the editor context menu and the Shift+F keybind.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CyberTimon

Copy link
Copy Markdown
Owner

LGTM! Thanks so much for your work on this, much appreciated! 🥇

@CyberTimon
CyberTimon merged commit 5fb4c29 into CyberTimon:main Oct 28, 2026
12 of 16 checks 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.

3 participants