From 755c87f9181f8743000cc4530e1ab474a0820c53 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 11 Oct 2026 11:48:29 +0200 Subject: [PATCH] fix: left-align thumbnails and tuck text beside the image --- src/main.rs | 9 ++++++--- src/thumbnail.rs | 36 ++++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index a4a3c68..8cf8536 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,8 @@ //! 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. //! Images use native terminal graphics when supported, otherwise colored half-block characters. -//! Thumbnails scale proportionally in the left pane, aligned one column from the text. +//! Thumbnails scale proportionally and align to the popup's left inner edge. +//! Left-aligned text starts one column after the actual image width, including height-limited images. //! A blank row separates the bold title from the dimmed author · publication date. //! Text retains a two-column right gutter; overflowing excerpts end with … without splitting wide glyphs. //! Both previews share asynchronous full-description fetching and per-video caching. @@ -1031,6 +1032,10 @@ impl App { Constraint::Fill(2), ]) .areas(block.inner(popup)); + let image_width = self.thumbnail.draw(frame, image); + let reclaimed = image.width - image_width; + details.x -= reclaimed; + details.width += reclaimed; details.width = details.width.saturating_sub(2); let metadata = Paragraph::new(vec![ Line::raw(video.title.as_str()).bold(), @@ -1102,8 +1107,6 @@ impl App { .set_symbol("…") .set_style(Style::new().dim()); } - // Drawing polls completion and starts resize work before we choose the title state. - self.thumbnail.draw(frame, image); frame.render_widget( block.title(loading_title( " Thumbnail ", diff --git a/src/thumbnail.rs b/src/thumbnail.rs index fc9739a..543e8ac 100644 --- a/src/thumbnail.rs +++ b/src/thumbnail.rs @@ -96,23 +96,25 @@ impl Thumbnail { .is_some_and(|image| image.complete && image.protocol.protocol_type().is_some()) } - pub fn draw(&mut self, frame: &mut Frame, area: Rect) { + /// Draw the image left-aligned and return its occupied width for adjacent content. + pub fn draw(&mut self, frame: &mut Frame, mut area: Rect) -> u16 { self.poll(); if area.width == 0 || area.height == 0 || self.selected.is_none() { - return; + return area.width; } let mut message = self.error.as_deref().unwrap_or("Loading thumbnail..."); if let Some(image) = self.image.as_mut() { let resize = Resize::Scale(Some(FilterType::Triangle)); if let Some(size) = image.protocol.size_for(resize.clone(), area.into()) { + area = left_aligned(area, size); frame.render_stateful_widget( StatefulImage::new().resize(resize), - right_aligned(area, size), + area, &mut image.protocol, ); if image.protocol.protocol_type().is_some() { - return; + return area.width; } } message = "Preparing thumbnail..."; @@ -130,6 +132,7 @@ impl Thumbnail { area.height - area.height / 2, ), ); + area.width } fn poll(&mut self) { @@ -235,15 +238,10 @@ fn decode(bytes: Vec) -> Result { .map_err(|error| format!("Could not decode thumbnail JPEG: {error}")) } -fn right_aligned(area: Rect, size: Size) -> Rect { +fn left_aligned(area: Rect, size: Size) -> Rect { let width = size.width.min(area.width); let height = size.height.min(area.height); - Rect::new( - area.x + area.width - width, - area.y + (area.height - height) / 2, - width, - height, - ) + Rect::new(area.x, area.y + (area.height - height) / 2, width, height) } #[cfg(test)] @@ -309,7 +307,11 @@ mod tests { }); 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(); + terminal + .draw(|frame| { + thumbnail.draw(frame, area); + }) + .unwrap(); assert!(thumbnail.is_loading()); let response = requests_rx .recv_timeout(Duration::from_secs(2)) @@ -317,7 +319,12 @@ mod tests { .resize_encode() .unwrap(); replies_tx.send(Ok(response)).unwrap(); - terminal.draw(|frame| thumbnail.draw(frame, area)).unwrap(); + let mut occupied_width = 0; + terminal + .draw(|frame| { + occupied_width = thumbnail.draw(frame, area); + }) + .unwrap(); assert!(!thumbnail.is_loading()); let buffer = terminal.backend().buffer(); let pixels: Vec<_> = (0..40) @@ -332,7 +339,8 @@ mod tests { 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_eq!(right, area.right()); // Keep the image against the metadata-facing edge. + assert_eq!(left, area.x); + assert_eq!(occupied_width, right - left); assert!(top >= area.y && bottom <= area.bottom()); assert!(right - left == area.width || bottom - top == area.height); assert!((top - area.y).abs_diff(area.bottom() - bottom) <= 1);