fix: use the layout information from television.ui_state to resize with the optimal size

- add argument preview_window in those functions/methods:
	Previewer.preview -> Previewer.dispatch_request -> FilePreviewer.preview ->  FilePreviewer.handle_preview_request -> try_preview -> Image.from_dynamic_image
   for try_preview, #[allow(clippy::too_many_arguments)] has been added since it has 8 arguments, pehraph would it be better to create a struct instead
 - resize image if possible with the size of the preview window
 - get_known_image_file_extensions function has been changed to follow the way of the text equivalent
This commit is contained in:
azy 2025-03-05 16:56:40 +09:00
parent 32c4c042e7
commit 6e6ce28bf5
5 changed files with 84 additions and 48 deletions

View File

@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::channels::entry::{Entry, PreviewType}; use crate::channels::entry::{Entry, PreviewType};
use devicons::FileIcon; use devicons::FileIcon;
use ratatui::layout::Rect;
pub mod ansi; pub mod ansi;
pub mod cache; pub mod cache;
@ -208,11 +209,15 @@ impl Previewer {
} }
} }
fn dispatch_request(&mut self, entry: &Entry) -> Option<Arc<Preview>> { fn dispatch_request(
&mut self,
entry: &Entry,
preview_window: Option<Rect>,
) -> Option<Arc<Preview>> {
match &entry.preview_type { match &entry.preview_type {
PreviewType::Basic => Some(self.basic.preview(entry)), PreviewType::Basic => Some(self.basic.preview(entry)),
PreviewType::EnvVar => Some(self.env_var.preview(entry)), PreviewType::EnvVar => Some(self.env_var.preview(entry)),
PreviewType::Files => self.file.preview(entry), PreviewType::Files => self.file.preview(entry, preview_window),
PreviewType::Command(cmd) => self.command.preview(entry, cmd), PreviewType::Command(cmd) => self.command.preview(entry, cmd),
PreviewType::None => Some(Arc::new(Preview::default())), PreviewType::None => Some(Arc::new(Preview::default())),
} }
@ -222,17 +227,23 @@ impl Previewer {
// faster, but since it's already running in the background and quite // faster, but since it's already running in the background and quite
// fast for most standard file sizes, plus we're caching the previews, // fast for most standard file sizes, plus we're caching the previews,
// I'm not sure the extra complexity is worth it. // I'm not sure the extra complexity is worth it.
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> { pub fn preview(
&mut self,
entry: &Entry,
preview_window: Option<Rect>,
) -> Option<Arc<Preview>> {
// if we haven't acknowledged the request yet, acknowledge it // if we haven't acknowledged the request yet, acknowledge it
self.requests.push(entry.clone()); self.requests.push(entry.clone());
if let Some(preview) = self.dispatch_request(entry) { if let Some(preview) = self.dispatch_request(entry, preview_window) {
return Some(preview); return Some(preview);
} }
// lookup request stack and return the most recent preview available // lookup request stack and return the most recent preview available
for request in self.requests.back_to_front() { for request in self.requests.back_to_front() {
if let Some(preview) = self.dispatch_request(&request) { if let Some(preview) =
self.dispatch_request(&request, preview_window)
{
return Some(preview); return Some(preview);
} }
} }

View File

@ -2,6 +2,7 @@ use crate::utils::files::{read_into_lines_capped, ReadResult};
use crate::utils::syntax::HighlightedLines; use crate::utils::syntax::HighlightedLines;
use image::ImageReader; use image::ImageReader;
use parking_lot::Mutex; use parking_lot::Mutex;
use ratatui::layout::Rect;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use std::collections::HashSet; use std::collections::HashSet;
use std::fs::File; use std::fs::File;
@ -11,7 +12,6 @@ use std::sync::{
atomic::{AtomicU8, Ordering}, atomic::{AtomicU8, Ordering},
Arc, Arc,
}; };
use syntect::{highlighting::Theme, parsing::SyntaxSet}; use syntect::{highlighting::Theme, parsing::SyntaxSet};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
@ -80,20 +80,28 @@ impl FilePreviewer {
self.cache.lock().get(&entry.name) self.cache.lock().get(&entry.name)
} }
pub fn preview(&mut self, entry: &entry::Entry) -> Option<Arc<Preview>> { pub fn preview(
&mut self,
entry: &entry::Entry,
preview_window: Option<Rect>,
) -> Option<Arc<Preview>> {
if let Some(preview) = self.cached(entry) { if let Some(preview) = self.cached(entry) {
trace!("Preview cache hit for {:?}", entry.name); trace!("Preview cache hit for {:?}", entry.name);
if preview.partial_offset.is_some() { if preview.partial_offset.is_some() {
// preview is partial, spawn a task to compute the next chunk // preview is partial, spawn a task to compute the next chunk
// and return the partial preview // and return the partial preview
debug!("Spawning partial preview task for {:?}", entry.name); debug!("Spawning partial preview task for {:?}", entry.name);
self.handle_preview_request(entry, Some(preview.clone())); self.handle_preview_request(
entry,
Some(preview.clone()),
preview_window,
);
} }
Some(preview) Some(preview)
} else { } else {
// preview is not in cache, spawn a task to compute the preview // preview is not in cache, spawn a task to compute the preview
trace!("Preview cache miss for {:?}", entry.name); trace!("Preview cache miss for {:?}", entry.name);
self.handle_preview_request(entry, None); self.handle_preview_request(entry, None, preview_window);
None None
} }
} }
@ -102,6 +110,7 @@ impl FilePreviewer {
&mut self, &mut self,
entry: &entry::Entry, entry: &entry::Entry,
partial_preview: Option<Arc<Preview>>, partial_preview: Option<Arc<Preview>>,
preview_window: Option<Rect>,
) { ) {
if self.in_flight_previews.lock().contains(&entry.name) { if self.in_flight_previews.lock().contains(&entry.name) {
trace!("Preview already in flight for {:?}", entry.name); trace!("Preview already in flight for {:?}", entry.name);
@ -128,6 +137,7 @@ impl FilePreviewer {
&syntax_theme, &syntax_theme,
&concurrent_tasks, &concurrent_tasks,
&in_flight_previews, &in_flight_previews,
preview_window,
); );
}); });
} }
@ -143,6 +153,7 @@ impl FilePreviewer {
/// This ends up being the max size of partial previews. /// This ends up being the max size of partial previews.
const PARTIAL_BUFREAD_SIZE: usize = 5 * 1024 * 1024; const PARTIAL_BUFREAD_SIZE: usize = 5 * 1024 * 1024;
#[allow(clippy::too_many_arguments)]
pub fn try_preview( pub fn try_preview(
entry: &entry::Entry, entry: &entry::Entry,
partial_preview: Option<Arc<Preview>>, partial_preview: Option<Arc<Preview>>,
@ -151,6 +162,7 @@ pub fn try_preview(
syntax_theme: &Arc<Theme>, syntax_theme: &Arc<Theme>,
concurrent_tasks: &Arc<AtomicU8>, concurrent_tasks: &Arc<AtomicU8>,
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>, in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
preview_window: Option<Rect>,
) { ) {
debug!("Computing preview for {:?}", entry.name); debug!("Computing preview for {:?}", entry.name);
let path = PathBuf::from(&entry.name); let path = PathBuf::from(&entry.name);
@ -246,8 +258,14 @@ pub fn try_preview(
debug!("File {:?} is an image", entry.name); debug!("File {:?} is an image", entry.name);
match ImageReader::open(path).unwrap().decode() { match ImageReader::open(path).unwrap().decode() {
Ok(image) => { Ok(image) => {
let preview_window_dimension = preview_window.map(|rect| {
(u32::from(rect.width), u32::from(rect.height))
});
let image_preview_widget = let image_preview_widget =
ImagePreviewWidget::from_dynamic_image(image); ImagePreviewWidget::from_dynamic_image(
image,
preview_window_dimension,
);
let total_lines = image_preview_widget let total_lines = image_preview_widget
.height() .height()
.try_into() .try_into()

View File

@ -304,7 +304,10 @@ impl Television {
&& !matches!(selected_entry.preview_type, PreviewType::None) && !matches!(selected_entry.preview_type, PreviewType::None)
{ {
// preview content // preview content
if let Some(preview) = self.previewer.preview(selected_entry) { if let Some(preview) = self
.previewer
.preview(selected_entry, self.ui_state.layout.preview_window)
{
// only update if the preview content has changed // only update if the preview content has changed
if self.preview_state.preview.title != preview.title { if self.preview_state.preview.title != preview.title {
self.preview_state.update( self.preview_state.update(

View File

@ -499,31 +499,26 @@ where
path.as_ref() path.as_ref()
.extension() .extension()
.and_then(|ext| ext.to_str()) .and_then(|ext| ext.to_str())
.is_some_and(|ext| KNOWN_IMAGE_FILE_EXTENSIONS.contains(ext)) .is_some_and(|ext| get_known_image_file_extensions().contains(ext))
} }
pub static KNOWN_IMAGE_FILE_EXTENSIONS: OnceLock<FxHashSet<&'static str>> =
lazy_static! { OnceLock::new();
static ref KNOWN_IMAGE_FILE_EXTENSIONS: FxHashSet<&'static str> = [ pub fn get_known_image_file_extensions() -> &'static FxHashSet<&'static str> {
KNOWN_IMAGE_FILE_EXTENSIONS.get_or_init(|| {
[
//"avif", //"avif",
//"bmp", //"bmp",
//"dds", //"dds",
//"farbfeld", //"farbfeld",
"gif", "gif", //"hdr",
//"hdr", "ico", "jpeg", "jpg", //"exr",
"ico", "png", //"pnm",
"jpeg",
"jpg",
//"exr",
"png",
//"pnm",
//"qoi", //"qoi",
//"tga", //"tga",
"tif", "tif", "tiff", "webp",
"tiff",
"webp",
] ]
.iter() .iter()
.copied() .copied()
.collect(); .collect()
})
} }

View File

@ -8,11 +8,11 @@ use std::fmt::Debug;
use std::hash::Hash; use std::hash::Hash;
static PIXEL_STRING: &str = ""; static PIXEL_STRING: &str = "";
const FILTER_TYPE: FilterType = FilterType::Lanczos3; const FILTER_TYPE: FilterType = FilterType::Triangle;
// use to reduce the size of the image before storing it // use to reduce the size of the image before storing it
const CACHED_WIDTH: u32 = 50; const DEFAULT_CACHED_WIDTH: u32 = 50;
const CACHED_HEIGHT: u32 = 100; const DEFAULT_CACHED_HEIGHT: u32 = 100;
const GRAY: Rgba<u8> = Rgba([242, 242, 242, 255]); const GRAY: Rgba<u8> = Rgba([242, 242, 242, 255]);
const WHITE: Rgba<u8> = Rgba([255, 255, 255, 255]); const WHITE: Rgba<u8> = Rgba([255, 255, 255, 255]);
@ -70,24 +70,33 @@ impl ImagePreviewWidget {
} }
} }
pub fn from_dynamic_image(dynamic_image: DynamicImage) -> Self { pub fn from_dynamic_image(
dynamic_image: DynamicImage,
dimension: Option<(u32, u32)>,
) -> Self {
// first quick resize // first quick resize
let big_resized_image = if dynamic_image.width() > CACHED_WIDTH * 4 let (window_width, window_height) =
|| dynamic_image.height() > CACHED_HEIGHT * 4 dimension.unwrap_or((DEFAULT_CACHED_WIDTH, DEFAULT_CACHED_HEIGHT));
let big_resized_image = if dynamic_image.width() > window_width * 4
|| dynamic_image.height() > window_height * 4
{ {
dynamic_image.resize( dynamic_image.resize(
CACHED_WIDTH * 4, window_width * 4,
CACHED_HEIGHT * 4, window_height * 4,
FilterType::Nearest, FilterType::Nearest,
) )
} else { } else {
dynamic_image dynamic_image
}; };
// this time resize with the filter // this time resize with the filter
let resized_image = if big_resized_image.width() > CACHED_WIDTH let resized_image = if big_resized_image.width() > window_width
|| big_resized_image.height() > CACHED_HEIGHT || big_resized_image.height() > window_height
{ {
big_resized_image.resize(CACHED_WIDTH, CACHED_HEIGHT, FILTER_TYPE) big_resized_image.resize(
window_width,
DEFAULT_CACHED_HEIGHT,
FILTER_TYPE,
)
} else { } else {
big_resized_image big_resized_image
}; };