diff --git a/src/converter.rs b/src/converter.rs index b87c94d..d7301fd 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,4 +1,7 @@ -use std::num::{NonZeroU32, NonZeroUsize}; +use std::{ + num::{NonZeroU32, NonZeroUsize}, + time::{SystemTime, UNIX_EPOCH} +}; use flume::{Receiver, SendError, Sender, TryRecvError}; use futures_util::stream::StreamExt; @@ -127,10 +130,15 @@ pub async fn run_conversion_loop( picker.font_size() ); + let rn = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let mut img = if shms_work { kittage::image::Image::shm_from( dyn_img, - &format!("__tdf_kittage_{pid}_page_{page_num}") + &format!("__tdf_kittage_{pid}_page_{rn}_{page_num}") ) .map_err(|e| RenderError::Converting(format!("Couldn't write to shm: {e}")))? } else { diff --git a/src/kitty.rs b/src/kitty.rs index 1fb7fbb..447e2ab 100644 --- a/src/kitty.rs +++ b/src/kitty.rs @@ -172,8 +172,6 @@ pub async fn display_kitty_images<'es>( match res { Ok(img_id) => { - // TODO: Re-add this or at least make sure this sort of thing does happen - // fake_image.unlink_if_shm(); *img = MaybeTransferred::Transferred(img_id); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index a93e08d..08c7de4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; +#[derive(PartialEq)] pub enum PrerenderLimit { All, Limited(NonZeroUsize) @@ -13,3 +14,46 @@ pub mod kitty; pub mod renderer; pub mod skip; pub mod tui; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum FitOrFill { + Fit, + Fill +} + +pub struct ScaledResult { + width: f32, + height: f32, + scale_factor: f32 +} + +pub fn scale_img_for_area( + (img_width, img_height): (f32, f32), + (area_width, area_height): (f32, f32), + fit_or_fill: FitOrFill +) -> ScaledResult { + // and get its aspect ratio + let img_aspect_ratio = img_width / img_height; + + // Then we get the full pixel dimensions of the area provided to us, and the aspect ratio + // of that area + let area_aspect_ratio = area_width / area_height; + + // and get the ratio that this page would have to be scaled by to fit perfectly within the + // area provided to us. + // we do this first by comparing the aspec ratio of the page with the aspect ratio of the + // area to fit it within. If the aspect ratio of the page is larger, then we need to scale + // the width of the page to fill perfectly within the height of the area. Otherwise, we + // scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly + // is scaled by the same factor as the dimension that _is_ scaled perfectly. + let scale_factor = match (img_aspect_ratio > area_aspect_ratio, fit_or_fill) { + (true, FitOrFill::Fit) | (false, FitOrFill::Fill) => area_width / img_width, + (false, FitOrFill::Fit) | (true, FitOrFill::Fill) => area_height / img_height + }; + + ScaledResult { + width: img_width * scale_factor, + height: img_height * scale_factor, + scale_factor + } +} diff --git a/src/main.rs b/src/main.rs index 0929481..30eb533 100644 --- a/src/main.rs +++ b/src/main.rs @@ -331,6 +331,9 @@ async fn enter_redraw_loop( InputAction::Search(term) => to_renderer.send(RenderNotif::Search(term))?, InputAction::Invert => to_renderer.send(RenderNotif::Invert)?, InputAction::Fullscreen => fullscreen = !fullscreen, + InputAction::SwitchRenderZoom(f_or_f) => { + to_renderer.send(RenderNotif::SwitchFitOrFill(f_or_f)).unwrap(); + } } } }, @@ -354,7 +357,12 @@ async fn enter_redraw_loop( } Some(img_res) = from_converter.next() => { match img_res { - Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results), + Ok(ConvertedPage { page, num, num_results }) => { + tui.page_ready(page, num, num_results); + if num == tui.page { + needs_redraw = true; + } + }, Err(e) => tui.show_error(e), } }, diff --git a/src/renderer.rs b/src/renderer.rs index 379ce70..a1e946a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -6,13 +6,17 @@ use mupdf::{ }; use ratatui::layout::Rect; -use crate::{PrerenderLimit, skip::InterleavedAroundWithMax}; +use crate::{ + FitOrFill, PrerenderLimit, ScaledResult, scale_img_for_area, skip::InterleavedAroundWithMax +}; +#[derive(Debug)] pub enum RenderNotif { Area(Rect), JumpToPage(usize), PageNeedsReRender(usize), Search(String), + SwitchFitOrFill(FitOrFill), Reload, Invert } @@ -93,6 +97,7 @@ pub fn start_rendering( let mut stored_doc = None; let mut invert = false; let mut preserved_area = None; + let mut fit_or_fill = FitOrFill::Fit; let mut need_rerender = VecDeque::new(); @@ -192,6 +197,12 @@ pub fn start_rendering( fill_default(&mut rendered, n_pages.get()); continue 'render_pages; } + RenderNotif::SwitchFitOrFill(f_or_f) => + if f_or_f != fit_or_fill { + fit_or_fill = f_or_f; + fill_default(&mut rendered, n_pages.get()); + continue 'render_pages; + }, RenderNotif::JumpToPage(page) => { start_point = page; continue 'render_pages; @@ -287,6 +298,7 @@ pub fn start_rendering( invert, black, white, + fit_or_fill, (area_w, area_h) ) { // If that fn returned Some, that means it needed to be re-rendered for some @@ -406,7 +418,7 @@ pub fn start_rendering( // So now we've just *searched* all the pages but not necessarily rendered all of them. // So if there are any we have yet to render, we need to loop back to the beginning of // this loop to continue rendering all of them - if rendered.iter().any(|r| !r.successful) { + if rendered.iter().any(|r| !r.successful) && prerender == PrerenderLimit::All { continue; } @@ -430,6 +442,7 @@ struct RenderedContext { result_rects: Vec } +#[expect(clippy::too_many_arguments)] fn render_single_page_to_ctx( page: &Page, search_term: Option<&str>, @@ -437,6 +450,7 @@ fn render_single_page_to_ctx( invert: bool, black: i32, white: i32, + fit_or_fill: FitOrFill, (area_w, area_h): (f32, f32) ) -> Result { let result_rects = match prev_render.num_search_found { @@ -447,30 +461,14 @@ fn render_single_page_to_ctx( // then, get the size of the page let bounds = page.bounds()?; - let (p_width, p_height) = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0); + let page_dim = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0); - // and get its aspect ratio - let p_aspect_ratio = p_width / p_height; - - // Then we get the full pixel dimensions of the area provided to us, and the aspect ratio - // of that area - let area_aspect_ratio = area_w / area_h; - - // and get the ratio that this page would have to be scaled by to fit perfectly within the - // area provided to us. - // we do this first by comparing the aspec ratio of the page with the aspect ratio of the - // area to fit it within. If the aspect ratio of the page is larger, then we need to scale - // the width of the page to fill perfectly within the height of the area. Otherwise, we - // scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly - // is scaled by the same factor as the dimension that _is_ scaled perfectly. - let scale_factor = if p_aspect_ratio > area_aspect_ratio { - area_w / p_width - } else { - area_h / p_height - }; - - let surface_w = p_width * scale_factor; - let surface_h = p_height * scale_factor; + let scaled = scale_img_for_area(page_dim, (area_w, area_h), fit_or_fill); + let ScaledResult { + width: surface_w, + height: surface_h, + scale_factor + } = scaled; let colorspace = Colorspace::device_rgb(); let matrix = Matrix::new_scale(scale_factor, scale_factor); diff --git a/src/tui.rs b/src/tui.rs index 91d9424..8bef541 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -24,6 +24,7 @@ use ratatui::{ use ratatui_image::{FontSize, Image}; use crate::{ + FitOrFill, converter::{ConvertedImage, MaybeTransferred}, kitty::{KittyDisplay, KittyReadyToDisplay}, renderer::{RenderError, fill_default}, @@ -32,7 +33,7 @@ use crate::{ pub struct Tui { name: String, - page: usize, + pub page: usize, last_render: LastRender, bottom_msg: BottomMessage, // we use `prev_msg` to, for example, restore the 'search results' message on the bottom after @@ -73,7 +74,7 @@ struct PageConstraints { r_to_l: bool } -#[derive(Default)] +#[derive(Default, Debug)] struct Zoom { // just how much 'zoom' you have. Doesn't relate to anything specific yet, except that 0 means // it fills the screen (instead of fits) @@ -89,7 +90,7 @@ struct Zoom { // This seems like a kinda weird struct because it holds two optionals but any representation // within it is valid; I think it's the best way to represent it #[derive(Default)] -struct RenderedInfo { +pub struct RenderedInfo { // The image, if it has been rendered by `Converter` to that struct img: Option, // The number of results for the current search term that have been found on this page. None if @@ -179,7 +180,7 @@ impl Tui { frame.render_widget(Skip::new(true), img_area); KittyDisplay::NoChange } else { - if let Some(ref zoom) = self.zoom { + if let Some(ref mut zoom) = self.zoom { // yes this is ugly and I hate it. it's due to the limitations that currently exist // in the borrow checker. Once `-Zpolonius=next` is stabilized, we can rework this // to look like what we expect. @@ -191,27 +192,46 @@ impl Tui { .as_ref() .is_some_and(|c| matches!(c, ConvertedImage::Kitty { .. })) { - log::debug!("we're inside, it's kitty"); let Some(ConvertedImage::Kitty { ref mut img, area }) = self.rendered[self.page].img else { unreachable!() }; - let img_width = f32::from(area.width); - let img_height = f32::from(area.height); - let available_to_real_width_ratio = f32::from(img_area.width) / img_width; - let available_to_real_height_ratio = f32::from(img_area.height) / img_height; + // Ugh I don't like this logic. I wish we could simplify it. + let (cell_width, cell_height) = if area.width >= img_area.width + && area.height >= img_area.height + { + (f32::from(img_area.width), f32::from(img_area.height)) + } else { + let img_width = f32::from(area.width); + let img_height = f32::from(area.height); + let available_to_real_width_ratio = f32::from(img_area.width) / img_width; + let available_to_real_height_ratio = + f32::from(img_area.height) / img_height; - let (width, height) = if available_to_real_width_ratio > available_to_real_height_ratio { (img_width, img_height / available_to_real_width_ratio) } else { (img_width / available_to_real_height_ratio, img_height) - }; + } + }; - let width = (width * f32::from(font_size.0)) as u32; - let height = (height * f32::from(font_size.1)) as u32; + let width = (cell_width * f32::from(font_size.0)) as u32; + let height = (cell_height * f32::from(font_size.1)) as u32; + + self.last_render = LastRender { + rect: size, + pages_shown: 1, + unused_width: 0 + }; + + zoom.cell_pan_from_left = zoom + .cell_pan_from_left + .min(area.width.saturating_sub(cell_width as u16)); + zoom.cell_pan_from_top = zoom + .cell_pan_from_top + .min(area.height.saturating_sub(cell_height as u16)); return KittyDisplay::DisplayImages(vec![KittyReadyToDisplay { img, @@ -314,7 +334,7 @@ impl Tui { frame.render_widget(Image::new(page_img), img_area); None } - ConvertedImage::Kitty { img, area } => Some((img, Position { + ConvertedImage::Kitty { img, area: _ } => Some((img, Position { x: img_area.x, y: img_area.y })) @@ -609,12 +629,13 @@ impl Tui { Some(InputAction::Redraw) } 'z' => { - self.zoom = match self.zoom { - None => Some(Zoom::default()), - Some(_) => None + let (zoom, f_or_f) = match self.zoom { + None => (Some(Zoom::default()), FitOrFill::Fill), + Some(_) => (None, FitOrFill::Fit) }; + self.zoom = zoom; self.last_render.rect = Rect::default(); - Some(InputAction::Redraw) + Some(InputAction::SwitchRenderZoom(f_or_f)) } 'L' => { if let Some(z) = &mut self.zoom { @@ -853,7 +874,8 @@ pub enum InputAction { Search(String), QuitApp, Invert, - Fullscreen + Fullscreen, + SwitchRenderZoom(crate::FitOrFill) } #[derive(Copy, Clone)]