fix: scale thumbnails to fill the preview window

This commit is contained in:
2026-10-09 22:50:32 +02:00
parent 689f14a634
commit b48682d133
2 changed files with 62 additions and 1 deletions
+1
View File
@@ -9,6 +9,7 @@
//! Esc closes the popup; j/k loads the selected video's description and i returns to editing. //! Esc closes the popup; j/k loads the selected video's description and i returns to editing.
//! Normal-mode t toggles a thumbnail in the same-sized popup; j/k follows selection, Esc closes. //! Normal-mode t toggles a thumbnail in the same-sized popup; j/k follows selection, Esc closes.
//! Images use native terminal graphics when supported, otherwise colored half-block characters. //! Images use native terminal graphics when supported, otherwise colored half-block characters.
//! Thumbnails scale up or down proportionally to fill the popup without cropping.
//! curl fetches JPEGs lazily into memory; decoding and image resizing/encoding run off-thread. //! curl fetches JPEGs lazily into memory; decoding and image resizing/encoding run off-thread.
//! Channel categories live in `$XDG_CONFIG_HOME/tubular/config.toml`, or `~/.config/tubular/config.toml`. //! Channel categories live in `$XDG_CONFIG_HOME/tubular/config.toml`, or `~/.config/tubular/config.toml`.
//! Under `[categories]`, write `Rust = ["@RustVideos"]`; channel URLs and UC IDs also work. //! Under `[categories]`, write `Rust = ["@RustVideos"]`; channel URLs and UC IDs also work.
+61 -1
View File
@@ -95,7 +95,7 @@ impl Thumbnail {
let mut message = self.error.as_deref().unwrap_or("Loading thumbnail..."); let mut message = self.error.as_deref().unwrap_or("Loading thumbnail...");
if let Some(image) = self.image.as_mut() { if let Some(image) = self.image.as_mut() {
let resize = Resize::Fit(Some(FilterType::Triangle)); let resize = Resize::Scale(Some(FilterType::Triangle));
if let Some(size) = image.protocol.size_for(resize.clone(), area.into()) { if let Some(size) = image.protocol.size_for(resize.clone(), area.into()) {
frame.render_stateful_widget( frame.render_stateful_widget(
StatefulImage::new().resize(resize), StatefulImage::new().resize(resize),
@@ -274,6 +274,66 @@ mod tests {
assert!(thumbnail.error.is_none()); assert!(thumbnail.error.is_none());
} }
#[test]
fn small_thumbnail_fills_popup_proportionally_and_resizes() {
use ratatui::{Terminal, backend::TestBackend, style::Color};
use std::time::Duration;
let mut thumbnail = Thumbnail::new();
let font = thumbnail.picker.font_size();
let image = image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(
48,
36,
image::Rgb([240, 40, 70]),
));
let (requests_tx, requests_rx) = mpsc::channel::<ResizeRequest>();
let (replies_tx, replies_rx) = mpsc::channel();
thumbnail.selected = Some("thumbnail".into());
thumbnail.image = Some(ThumbnailImage {
id: "thumbnail".into(),
protocol: ThreadProtocol::new(
requests_tx,
Some(thumbnail.picker.new_resize_protocol(image)),
),
replies: replies_rx,
complete: false,
});
let mut terminal = Terminal::new(TestBackend::new(100, 40)).unwrap();
for area in [Rect::new(3, 2, 80, 18), Rect::new(7, 4, 30, 8)] {
terminal.draw(|frame| thumbnail.draw(frame, area)).unwrap();
let response = requests_rx
.recv_timeout(Duration::from_secs(2))
.unwrap()
.resize_encode()
.unwrap();
replies_tx.send(Ok(response)).unwrap();
terminal.draw(|frame| thumbnail.draw(frame, area)).unwrap();
let buffer = terminal.backend().buffer();
let pixels: Vec<_> = (0..40)
.flat_map(|y| (0..100).map(move |x| (x, y)))
.filter(|&(x, y)| {
let cell = &buffer[(x, y)];
matches!(cell.fg, Color::Rgb(..)) || matches!(cell.bg, Color::Rgb(..))
})
.collect();
let left = pixels.iter().map(|&(x, _)| x).min().unwrap();
let right = pixels.iter().map(|&(x, _)| x).max().unwrap() + 1;
let top = pixels.iter().map(|&(_, y)| y).min().unwrap();
let bottom = pixels.iter().map(|&(_, y)| y).max().unwrap() + 1;
assert!(left >= area.x && right <= area.right());
assert!(top >= area.y && bottom <= area.bottom());
assert!(right - left == area.width || bottom - top == area.height);
assert!((left - area.x).abs_diff(area.right() - right) <= 1);
assert!((top - area.y).abs_diff(area.bottom() - bottom) <= 1);
let width = u32::from(right - left) * u32::from(font.width);
let height = u32::from(bottom - top) * u32::from(font.height);
assert!(
width.abs_diff(height * 4 / 3)
<= u32::from(font.width) + u32::from(font.height) * 4 / 3
);
}
}
#[test] #[test]
fn resized_image_preserves_aspect_and_stays_inside_popup() { fn resized_image_preserves_aspect_and_stays_inside_popup() {
let picker = Picker::halfblocks(); let picker = Picker::halfblocks();