Repository navigation
feat(workspace): pop-out viewer window and long-document navigation - #142
Merged
Merged
Conversation
A Pop out button in the Details image pane opens a second window: the full viewer on a dark stage with a Details sidebar beside it, following the table's selection so the table can sit on one monitor and the scan on the other. - Title bar: Following/Pinned toggle, row stepping, "Row 42 of 318", and a "Table is on row N · Jump" chip while pinned. Pins hold row ids. - Sidebar: DetailsPanel without its image pane, resizable 240-720px and collapsible; documents get a Details | Find tab strip (Ctrl+F). - Stage: the viewer in a new Scope::PopOut, plus nothing-selected, missing-file and no-preview states, and a stack with Alt+Left/Right for several selected rows. - One window per project; closes with the project or the main window, and reopens at its saved size on its saved display.
- Stop playback when the pop-out leaves or closes a file, and only when the shared player is playing that file, so the main window's recording is never silenced. - Keep the window's bounds per project through MainWindowBounds, the same mechanism and `.qrate` key pattern as the main window, instead of a new app-wide setting. - Re-find a pin only when rows were added, removed or moved (one id map per structural change), and compare the grid's cursor rather than its whole selection when drawing the "Table is on row N" chip.
…by id - The viewer waits briefly before probing pages or duration, and holds the task, so stepping quickly through files starts one ffmpeg rather than one per file. - Find waits for typing to settle and replaces the previous search task, so a word typed quickly runs one PDFium search. - The Details editor captures row ids and resolves them, and its column, at commit, so a row added or removed mid-edit cannot redirect the write.
devnull03
marked this pull request as ready for review
September 28, 2026 19:09
# Conflicts: # crates/workspace/src/panels/details.rs # crates/workspace/src/viewer/mod.rs
- Playback records the view whose transport started it; the pop-out stops only playback its own viewer started, so the main window playing the same file keeps going. - Keep the latest pop-out bounds in memory per project and prefer them over the debounced .qrate write, so a quick reopen gets the current size and display. - Drop deleted items from a pin when it is re-found, so later cell edits take the cheap path instead of rebuilding the id map each time. - Open at 1120x700 when nothing is saved and never below the 800x600 minimum, rather than MainWindowBounds' 600x800 main-window fallback.
…zoom The viewer's image had no element id, so gpui kept no frame clock and a GIF stood on its first frame. Only the fullscreen viewer animates now; the Details pane and gallery get the first frame through the cached ladder. - Thumbnails honour EXIF orientation, as gpui already did for the viewer; the thumbnail cache key is salted so sideways entries are rebuilt - Captions carry pixel dimensions, read from the header - Viewer: quarter-turn rotation (R / Shift+R), a zoom readout that toggles fit and 1:1, double-click to zoom, pan clamped to the image, and open/closed hand cursors while it can be panned
5 tasks
overflow_hidden clips to a rectangle in gpui, so a Cover image's square corners showed past the launcher's rounded border.
…ption The pop-out button now lights its glyph like the status bar instead of filling a box. The caption chip caps its width and ellipsizes, so a narrow pane cannot run it under the action buttons.
…omed out A turn re-decodes the file, since gpui cannot rotate an image sprite, so the stage went blank until it landed. The viewer now draws the last finished turn until the new one is ready. Panning is allowed again below fit, bounded by the frame. Windows has no grab cursors in gpui, so the hand stands in there. Turns and preview decodes are logged at debug level with their timings. The details caption drops the pixel dimensions and shows type and size only.
…ring the cache Clear cache only deleted the files on disk, so every thumbnail already decoded stayed in memory and nothing changed on screen. It now drops the decoded pictures too, and they are drawn again from the files. While previews decode, the right of the status bar shows a spinner and how many are in flight, so a gallery filling in reads as work under way.
It rendered empty but still counted as occupied, so the bar kept a divider and a gap for it.
It was the one opaque, bordered, shadowed pill with outline buttons and bold text. It now uses the translucent pill and small ghost buttons the toolbar and row stepper use. The video scrubber shares it.
The page readout in the viewer's pill is now a box: Enter jumps to the typed page, clamped to the document, and Escape puts the current page back. Works in the overlay and the pop-out, which share the pill. Part of #144.
A column of page thumbnails on the left of the viewer's stage, for PDFs and TIFF stacks. It is a virtualised list, so only the pages on screen are rendered, through the same cached thumbnail path as the gallery. The current page is outlined and kept in view, and a click goes to it. The page pill has a button to hide it. It lives in the stage rather than the pop-out's sidebar, so the overlay and the pop-out get the same strip. Part of #144.
5 tasks
A button in the page pill, and W, zoom a page until it spans the frame and start at its top. The page's shape comes from the picture that was decoded, so a PDF or TIFF page works without asking PDFium for a size, and actual size (1) now works on documents too. Part of #144.
The page pill's button opens it.
The outline and rounding were on a fixed box around the thumbnail, so every page showed as the same shape. They are on the picture now, which keeps its own aspect ratio inside the row.
Capping the picture with max_w_full/max_h_full inside a flexed row left it at its intrinsic 512px: it spilled past the strip's edge and the outlined page came up empty. Each thumbnail is now given an explicit size, its decoded shape fitted to the row, and a portrait placeholder until it lands.
Page one of a TIFF stack is handed to gpui, which reports no size, so paging back to it kept the previous page's shape for fit, pan and fit to width. The viewer now falls back to the header's size for it.
A turn is decoded by us rather than gpui and held under the preview budget; a 60 MP scan held whole was larger than the budget on its own, so it and the gallery evicted each other. The shrink happens after the decode, so nothing extra is written to the disk cache.
It re-rendered its title bar, stage and sidebar each time the viewer notified, which is every mouse move of a drag. It now redraws only when the pill, the document tabs or the find results change. The stage skips the missing-file lookup while a file is showing, and the OS title is set only when it changes.
The floor was a flat 10%, so an icon fitted far past 10x could not get down to one pixel per pixel and 1 showed 312%. The floor now widens to the actual size, as the ceiling already did.
Every viewer built the input and its subscription on first render, including photos, recordings and each row stepped through in the pop-out.
While a turn decoded, fit and pan limits already used the new shape though the old picture was still shown. They now follow the turn on screen. A page change drops the old turn too, since the new page has none decoded and the stage would otherwise wait on it blank.
Zooming at or below fit snapped the picture back to the centre, so a picture dragged inside its frame jumped on the next wheel tick. Zoom now keeps the pan, clamped to the frame. Fit (0, or the readout) still recentres, through one fit_view shared with page changes and turns.
Closing the full-screen viewer, or moving the Details selection, stopped whatever was playing, including a pinned pop-out's recording. The owner check the pop-out had is now playback::stop itself, so every caller gets it; the viewer also stops its own recording when the next row's replaces it.
The Cell-or-Row match was written out in five places across the grid, the viewer, the pop-out and the view switcher. It is now QrateTableDelegate::cursor_row.
The viewer's row arrows and the pinned pop-out's each looked up the view row, stepped and tested for a previewable file. Both now call viewer::next_previewable.
The pop-out's stack copied Details' clamp-to-last and wrap-around maths verbatim. Both now use details::stack_front and stack_step.
The id-to-row map was built by hand in four places and scanned for in two more, and rows were turned into ids through row_ids().get() in three. The delegate now has row_positions, row_of and a public row_id.
The look-up, upgrade and update was written out at eight sites across five crates, and the grid's menu handlers kept a private copy. They all use one public TablePanelHandle::update now.
The silent-WAV header was built by hand in four tests across two crates; it is preview::playback::silent_wav now, behind a test-support feature workspace's tests turn on. The Details and pop-out tests share test_support::open_table, and the table panel's tests start from one project with autosave already off.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #141 (Notion ID: 3e921d32-b13b-8129-9f75-ef3977842a5d)
Closes #144 (Notion ID: 3e921d32-b13b-81e2-ae04-d612d56cac51)
Implements the Pop-out Viewer design handoff. A Pop out button in the Details image pane opens a second window. The window shows the selected row's file at full size, with its fields beside it. It follows the table's selection, so the table can sit on one monitor and the scan on the other.
Scope::PopOut, on the same black-85% backdrop. Zoom, pan, paging, the video scrubber, the audio transport and the install banner work as they do in the full-screen viewer. The stage also has its own states:DetailsPanelwithout its image pane. It's 320px by default, can be dragged from 240 to 720px, and can be collapsed. Documents get a Details | Find tab strip, and Ctrl+F switches to Find. Esc goes back to Details and does nothing further, so it doesn't deselect the grid from this window.MainWindowBounds, the same way the main window saves its own.Long-document navigation (#144)
These work in the full-screen viewer and in the pop-out, which share the page pill and the stage.
uniform_list, so only the pages on screen are rendered, through the same cached thumbnail path as the gallery. The current page is outlined and kept in view, and a click goes to it. It starts closed, and a button in the page pill opens it. It's in the stage rather than a pop-out sidebar tab, so both views get the same strip.W. It zooms the page to span the frame and starts at its top. The page's shape comes from the decoded picture rather than from PDFium, so it works for PDF and TIFF pages alike, and1(actual size) now works on documents too.Viewer and preview fixes
These affect both the pop-out stage and the full-screen overlay.
overflow_hiddenclips to a rectangle in gpui, so a cover image's corners showed past a rounded border (the launcher's recent projects).JPG · 725.3 KB, and truncates rather than running under the action buttons in a narrow pane.Rto turn clockwise andShift+Rto turn back. It only changes the view and resets per file. gpui can't rotate an image sprite, so a turn decodes the file again; the last finished turn stays on screen until the new one is ready, so the stage never goes blank. A rotated GIF stops animating, and a rotated page shows no search highlights.1jumps to 1:1. The zoom limit rises above 8× when 1:1 needs it.Status bar and cache
Differences from the tasks
Viewer::load: the viewer is rebuilt on each file change, the same way the overlay already works.open_vieweris split intobuildand mounting, rather than getting aViewer::load.MainWindowBoundsmechanism doesn't save the position.component_banner.Verification
./scripts/ci.sh(fmt, clippy with-D warnings -A dead_code, andcargo test --workspace) on Windows at b955d06. All three passed.🤖 Generated with Claude Code