Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1725ebe
feat(workspace): add a pop-out viewer window that follows the selection
devnull03 Sep 28, 2026
2c3b3ff
fix(workspace): pop-out playback, per-project placement and pin cost
devnull03 Sep 28, 2026
4c3072e
fix(workspace): settle viewer probes and searches; aim Details edits …
devnull03 Sep 28, 2026
1e7159d
Merge remote-tracking branch 'origin/main' into feat/pop-out-viewer
devnull03 Sep 28, 2026
52b2156
fix(workspace): pop-out playback ownership, fresh bounds and pin pruning
devnull03 Sep 28, 2026
ac1770e
feat(workspace): animate GIFs in the viewer, rotate, and actual-size …
devnull03 Sep 28, 2026
9772ea9
fix(preview): round thumbnail images so corners stay inside their frame
devnull03 Sep 29, 2026
4d18004
fix(workspace): tint the pop-out button when open and truncate the ca…
devnull03 Sep 29, 2026
0597986
fix(workspace): keep the picture up while a turn decodes; pan when zo…
devnull03 Sep 29, 2026
0ca6ff4
feat(app): show preview decoding in the status bar; redraw after clea…
devnull03 Sep 29, 2026
46b5802
fix(app): drop the previews readout from the status bar when idle
devnull03 Sep 29, 2026
589c1c7
style(workspace): match the viewer's page pill to its other controls
devnull03 Sep 29, 2026
efdd335
feat(workspace): go to a page by typing its number
devnull03 Sep 29, 2026
e85b523
feat(workspace): add a page strip beside multi-page documents
devnull03 Sep 29, 2026
481846a
feat(workspace): fit a page to the viewer's width
devnull03 Sep 29, 2026
4feaa87
fix(workspace): start the page strip closed
devnull03 Sep 29, 2026
2872725
fix(workspace): keep each page's shape in the page strip
devnull03 Sep 29, 2026
2636fe5
fix(workspace): size page-strip thumbnails from the decoded page
devnull03 Sep 29, 2026
6e4e335
fix(workspace): measure a file drawn by gpui from its header again
devnull03 Sep 29, 2026
227a7a6
perf(preview): cap a turned full-size picture at 4096px
devnull03 Sep 29, 2026
b3540c3
perf(workspace): stop the pop-out redrawing on every pan and zoom
devnull03 Sep 29, 2026
edcef95
fix(workspace): let zoom reach actual size on tiny images
devnull03 Sep 29, 2026
8902fca
perf(workspace): build the page box only for files with a page pill
devnull03 Sep 29, 2026
adb1ce7
fix(workspace): measure zoom and pan on the turn that is on screen
devnull03 Sep 29, 2026
898fa54
fix(workspace): keep the pan when zooming below fit
devnull03 Sep 29, 2026
ab87e55
fix(workspace): stop only the recording a view started
devnull03 Sep 29, 2026
e82b273
refactor(table): name the row the grid's cursor is on
devnull03 Sep 29, 2026
49c62a9
refactor(workspace): share the step to the next previewable row
devnull03 Sep 29, 2026
448b7ef
refactor(workspace): share the selection stack's front and wrap
devnull03 Sep 29, 2026
cf25783
refactor(table): convert between row ids and rows in one place
devnull03 Sep 29, 2026
f3f9728
refactor(table): reach the centre table through TablePanelHandle::update
devnull03 Sep 29, 2026
b955d06
test: share the WAV and table fixtures
devnull03 Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/icons/app-window.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/file-x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/move-horizontal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/pin-filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion crates/app/src/app_settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ fn previews_group(cx: &App) -> SettingGroup {
};
cx.update(|cx| {
cx.set_global(CacheCleared(outcome.into()));
cx.refresh_windows();
preview::forget_all(cx);
});
})
.detach();
Expand Down
25 changes: 23 additions & 2 deletions crates/app/src/assets.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! Our own icons in front of `gpui_component_assets`, which is otherwise the only asset source.
//! The bundled icon set has no filled panel glyphs (the title bar's "this dock is open" state) and
//! no picture glyph (visual search), so those SVGs are ours, copied from Lucide.
//! no picture glyph (visual search), and none of the pop-out viewer's window, pin or missing-file
//! glyphs, nor the viewer's fit-to-width glyph, so those SVGs are ours, copied from Lucide.

use std::borrow::Cow;

use gpui::{AssetSource, Result, SharedString};

pub struct Assets;

const OWN: [(&str, &str); 5] = [
const OWN: [(&str, &str); 10] = [
(
"icons/history.svg",
include_str!("../../../assets/icons/history.svg"),
Expand All @@ -29,6 +30,26 @@ const OWN: [(&str, &str); 5] = [
"icons/image.svg",
include_str!("../../../assets/icons/image.svg"),
),
(
"icons/app-window.svg",
include_str!("../../../assets/icons/app-window.svg"),
),
(
"icons/pin.svg",
include_str!("../../../assets/icons/pin.svg"),
),
(
"icons/pin-filled.svg",
include_str!("../../../assets/icons/pin-filled.svg"),
),
(
"icons/file-x.svg",
include_str!("../../../assets/icons/file-x.svg"),
),
(
"icons/move-horizontal.svg",
include_str!("../../../assets/icons/move-horizontal.svg"),
),
];

impl AssetSource for Assets {
Expand Down
2 changes: 2 additions & 0 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ impl App {
return false;
}
flush_all_state(cx);
// Its table goes with this window, and on Windows and Linux an open window keeps the app running.
workspace::close_pop_out(cx);
true
});

Expand Down
9 changes: 9 additions & 0 deletions crates/app/src/status_items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod markup;
mod new_files;
mod panel_buttons;
mod plugin_bar;
mod previews_busy;

use cell_location::CellLocation;
use gpui::*;
Expand All @@ -11,6 +12,7 @@ use new_files::NewFilesButton;
use panel_buttons::PanelButtons;
use plugin_api::{Bar, BarContributions, Side};
pub use plugin_bar::PluginBar;
use previews_busy::PreviewsBusy;
use window_wrapper::{BarRegistry, status_bar::StatusBarRegistry};
use workspace::{BarSide, DockToggleButton, PANELS};

Expand Down Expand Up @@ -64,6 +66,13 @@ pub fn build_status_bar_registry(cx: &mut App, dock: WeakEntity<DockArea>) -> St
!BarContributions::at(Bar::Status, Side::Right, cx).is_empty()
});

let previews_busy = cx.new(PreviewsBusy::new);
registry
.items_mut()
.add_right_if(previews_busy.clone(), move |cx| {
previews_busy.read(cx).shown > 0
});

// Text readout of the table's selected cell.
let cell_location = cx.new(CellLocation::new);
registry.items_mut().add_right(cell_location);
Expand Down
7 changes: 1 addition & 6 deletions crates/app/src/status_items/new_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ impl Render for NewFilesButton {
.label(format!("New files ({count})"))
.tooltip("Files in the files folder that no row links to")
.on_click(|_, window, cx| {
if let Some(table) = cx
.try_global::<TablePanelHandle>()
.and_then(|handle| handle.0.upgrade())
{
table.update(cx, |table, cx| table.import_new_files(window, cx));
}
TablePanelHandle::update(cx, |table, cx| table.import_new_files(window, cx));
})
.into_any_element()
}
Expand Down
51 changes: 51 additions & 0 deletions crates/app/src/status_items/previews_busy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Right-side status-bar readout: a spinner and a count while previews are decoding, so a gallery
//! filling in reads as work under way rather than a slow app.

use std::time::Duration;

use gpui::*;
use gpui_component::{Sizable as _, h_flex, spinner::Spinner};

pub struct PreviewsBusy {
pub shown: usize,
_poll: Task<()>,
}

impl PreviewsBusy {
/// ponytail: polls, since decodes finish on a background thread that cannot notify a view.
/// Four cheap loads a second, and a re-render only when the count moves.
pub fn new(cx: &mut Context<Self>) -> Self {
let _poll = cx.spawn(async move |this, cx| {
loop {
cx.background_executor()
.timer(Duration::from_millis(250))
.await;
let now = preview::decoding();
let alive = this.update(cx, |this, cx| {
if this.shown == now {
return;
}
// Appearing or leaving changes the bar's dividers, which only the bar redraws.
if (this.shown == 0) != (now == 0) {
cx.refresh_windows();
}
this.shown = now;
cx.notify();
});
if alive.is_err() {
break;
}
}
});
Self { shown: 0, _poll }
}
}

impl Render for PreviewsBusy {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1()
.child(Spinner::new().xsmall())
.child(format!("Loading previews ({})", self.shown))
}
}
19 changes: 7 additions & 12 deletions crates/onboarding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,18 +706,13 @@ fn show_me(guide: &Entity<Guide>, task: Task, window: &mut Window, cx: &mut App)
}
Task::AddFilesFolder => {
let blank = guide.read(cx).kind == GuideKind::Blank;
if let Some(table) = cx
.try_global::<table::TablePanelHandle>()
.and_then(|handle| handle.0.upgrade())
{
table.update(cx, |table, cx| {
if blank {
table.choose_import_paths(window, cx)
} else {
table.choose_files_root(window, cx)
}
});
}
table::TablePanelHandle::update(cx, |table, cx| {
if blank {
table.choose_import_paths(window, cx)
} else {
table.choose_files_root(window, cx)
}
});
}
Task::OpenRow | Task::SelectRow => {
ensure_visible(guide, DETAILS_META.name, window, cx);
Expand Down
4 changes: 4 additions & 0 deletions crates/preview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pdfium-render = { version = "0.9.3", default-features = false, features = ["pdfi
# ship two whole symphonia trees in the binary. Move both together or neither.
symphonia = { version = "0.5.5", default-features = false, features = ["mp3", "aac", "alac", "isomp4", "ogg", "vorbis", "flac", "wav", "pcm"] }

[features]
# Test fixtures other crates' tests use, such as `playback::silent_wav`.
test-support = []

[dev-dependencies]
# `#[gpui::test]`, TestAppContext, and run_test are all behind this feature.
gpui = { workspace = true, features = ["test-support"] }
Expand Down
17 changes: 1 addition & 16 deletions crates/preview/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,7 @@ mod tests {
/// rather than erroring in a way that would look the same as an unreadable file.
#[test]
fn a_valid_recording_without_artwork_is_not_an_error() {
// 44-byte canonical WAV header describing one sample of silence.
let mut wav = Vec::new();
wav.extend(b"RIFF");
wav.extend(36u32.to_le_bytes());
wav.extend(b"WAVEfmt ");
wav.extend(16u32.to_le_bytes());
wav.extend(1u16.to_le_bytes()); // PCM
wav.extend(1u16.to_le_bytes()); // mono
wav.extend(8000u32.to_le_bytes());
wav.extend(16000u32.to_le_bytes());
wav.extend(2u16.to_le_bytes());
wav.extend(16u16.to_le_bytes());
wav.extend(b"data");
wav.extend(2u32.to_le_bytes());
wav.extend(0u16.to_le_bytes());

let wav = crate::playback::silent_wav(1);
let path = std::env::temp_dir().join("qrate-audio-silent.wav");
std::fs::write(&path, &wav).unwrap();
assert!(audio::cover(&path).is_none(), "no artwork, but no panic");
Expand Down
5 changes: 5 additions & 0 deletions crates/preview/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ pub fn dir() -> Option<PathBuf> {
.clone()
}

/// Bumped when a decode changes what an unchanged file looks like, so every old entry misses.
/// 1: raster thumbnails turned upright by their EXIF orientation.
const FORMAT: u32 = 1;

/// Identity of one cached rendering. The file's length and mtime are in the hash, so editing or
/// replacing a source file misses rather than serving the old picture — which is why nothing here
/// needs an invalidation pass.
Expand All @@ -61,6 +65,7 @@ pub fn dir() -> Option<PathBuf> {
pub fn key(path: &Path, max_edge: u32, page: usize) -> Option<String> {
let meta = fs::metadata(path).ok()?;
let mut hasher = DefaultHasher::new();
FORMAT.hash(&mut hasher);
path.hash(&mut hasher);
meta.len().hash(&mut hasher);
meta.modified().ok()?.hash(&mut hasher);
Expand Down
Loading
Loading