fix: left-align thumbnails and tuck text beside the image
This commit is contained in:
+6
-3
@@ -17,7 +17,8 @@
|
|||||||
//! 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 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.
|
//! 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.
|
//! 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.
|
//! Both previews share asynchronous full-description fetching and per-video caching.
|
||||||
@@ -1031,6 +1032,10 @@ impl App {
|
|||||||
Constraint::Fill(2),
|
Constraint::Fill(2),
|
||||||
])
|
])
|
||||||
.areas(block.inner(popup));
|
.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);
|
details.width = details.width.saturating_sub(2);
|
||||||
let metadata = Paragraph::new(vec![
|
let metadata = Paragraph::new(vec![
|
||||||
Line::raw(video.title.as_str()).bold(),
|
Line::raw(video.title.as_str()).bold(),
|
||||||
@@ -1102,8 +1107,6 @@ impl App {
|
|||||||
.set_symbol("…")
|
.set_symbol("…")
|
||||||
.set_style(Style::new().dim());
|
.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(
|
frame.render_widget(
|
||||||
block.title(loading_title(
|
block.title(loading_title(
|
||||||
" Thumbnail ",
|
" Thumbnail ",
|
||||||
|
|||||||
+22
-14
@@ -96,23 +96,25 @@ impl Thumbnail {
|
|||||||
.is_some_and(|image| image.complete && image.protocol.protocol_type().is_some())
|
.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();
|
self.poll();
|
||||||
if area.width == 0 || area.height == 0 || self.selected.is_none() {
|
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...");
|
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::Scale(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()) {
|
||||||
|
area = left_aligned(area, size);
|
||||||
frame.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
StatefulImage::new().resize(resize),
|
StatefulImage::new().resize(resize),
|
||||||
right_aligned(area, size),
|
area,
|
||||||
&mut image.protocol,
|
&mut image.protocol,
|
||||||
);
|
);
|
||||||
if image.protocol.protocol_type().is_some() {
|
if image.protocol.protocol_type().is_some() {
|
||||||
return;
|
return area.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message = "Preparing thumbnail...";
|
message = "Preparing thumbnail...";
|
||||||
@@ -130,6 +132,7 @@ impl Thumbnail {
|
|||||||
area.height - area.height / 2,
|
area.height - area.height / 2,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
area.width
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll(&mut self) {
|
fn poll(&mut self) {
|
||||||
@@ -235,15 +238,10 @@ fn decode(bytes: Vec<u8>) -> Result<image::DynamicImage, String> {
|
|||||||
.map_err(|error| format!("Could not decode thumbnail JPEG: {error}"))
|
.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 width = size.width.min(area.width);
|
||||||
let height = size.height.min(area.height);
|
let height = size.height.min(area.height);
|
||||||
Rect::new(
|
Rect::new(area.x, area.y + (area.height - height) / 2, width, height)
|
||||||
area.x + area.width - width,
|
|
||||||
area.y + (area.height - height) / 2,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -309,7 +307,11 @@ mod tests {
|
|||||||
});
|
});
|
||||||
let mut terminal = Terminal::new(TestBackend::new(100, 40)).unwrap();
|
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)] {
|
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());
|
assert!(thumbnail.is_loading());
|
||||||
let response = requests_rx
|
let response = requests_rx
|
||||||
.recv_timeout(Duration::from_secs(2))
|
.recv_timeout(Duration::from_secs(2))
|
||||||
@@ -317,7 +319,12 @@ mod tests {
|
|||||||
.resize_encode()
|
.resize_encode()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
replies_tx.send(Ok(response)).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());
|
assert!(!thumbnail.is_loading());
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
let pixels: Vec<_> = (0..40)
|
let pixels: Vec<_> = (0..40)
|
||||||
@@ -332,7 +339,8 @@ mod tests {
|
|||||||
let top = pixels.iter().map(|&(_, y)| y).min().unwrap();
|
let top = pixels.iter().map(|&(_, y)| y).min().unwrap();
|
||||||
let bottom = pixels.iter().map(|&(_, y)| y).max().unwrap() + 1;
|
let bottom = pixels.iter().map(|&(_, y)| y).max().unwrap() + 1;
|
||||||
assert!(left >= area.x && right <= area.right());
|
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!(top >= area.y && bottom <= area.bottom());
|
||||||
assert!(right - left == area.width || bottom - top == area.height);
|
assert!(right - left == area.width || bottom - top == area.height);
|
||||||
assert!((top - area.y).abs_diff(area.bottom() - bottom) <= 1);
|
assert!((top - area.y).abs_diff(area.bottom() - bottom) <= 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user