yay zooming woohoo

This commit is contained in:
itsjunetime
2025-06-15 18:11:22 -06:00
parent a67ff7996c
commit 0578fccfa6
6 changed files with 127 additions and 49 deletions
+10 -2
View File
@@ -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 flume::{Receiver, SendError, Sender, TryRecvError};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
@@ -127,10 +130,15 @@ pub async fn run_conversion_loop(
picker.font_size() picker.font_size()
); );
let rn = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut img = if shms_work { let mut img = if shms_work {
kittage::image::Image::shm_from( kittage::image::Image::shm_from(
dyn_img, 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}")))? .map_err(|e| RenderError::Converting(format!("Couldn't write to shm: {e}")))?
} else { } else {
-2
View File
@@ -172,8 +172,6 @@ pub async fn display_kitty_images<'es>(
match res { match res {
Ok(img_id) => { 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); *img = MaybeTransferred::Transferred(img_id);
Ok(()) Ok(())
} }
+44
View File
@@ -3,6 +3,7 @@ use std::num::NonZeroUsize;
#[global_allocator] #[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[derive(PartialEq)]
pub enum PrerenderLimit { pub enum PrerenderLimit {
All, All,
Limited(NonZeroUsize) Limited(NonZeroUsize)
@@ -13,3 +14,46 @@ pub mod kitty;
pub mod renderer; pub mod renderer;
pub mod skip; pub mod skip;
pub mod tui; 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
}
}
+9 -1
View File
@@ -331,6 +331,9 @@ async fn enter_redraw_loop(
InputAction::Search(term) => to_renderer.send(RenderNotif::Search(term))?, InputAction::Search(term) => to_renderer.send(RenderNotif::Search(term))?,
InputAction::Invert => to_renderer.send(RenderNotif::Invert)?, InputAction::Invert => to_renderer.send(RenderNotif::Invert)?,
InputAction::Fullscreen => fullscreen = !fullscreen, 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() => { Some(img_res) = from_converter.next() => {
match img_res { 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), Err(e) => tui.show_error(e),
} }
}, },
+23 -25
View File
@@ -6,13 +6,17 @@ use mupdf::{
}; };
use ratatui::layout::Rect; 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 { pub enum RenderNotif {
Area(Rect), Area(Rect),
JumpToPage(usize), JumpToPage(usize),
PageNeedsReRender(usize), PageNeedsReRender(usize),
Search(String), Search(String),
SwitchFitOrFill(FitOrFill),
Reload, Reload,
Invert Invert
} }
@@ -93,6 +97,7 @@ pub fn start_rendering(
let mut stored_doc = None; let mut stored_doc = None;
let mut invert = false; let mut invert = false;
let mut preserved_area = None; let mut preserved_area = None;
let mut fit_or_fill = FitOrFill::Fit;
let mut need_rerender = VecDeque::new(); let mut need_rerender = VecDeque::new();
@@ -192,6 +197,12 @@ pub fn start_rendering(
fill_default(&mut rendered, n_pages.get()); fill_default(&mut rendered, n_pages.get());
continue 'render_pages; 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) => { RenderNotif::JumpToPage(page) => {
start_point = page; start_point = page;
continue 'render_pages; continue 'render_pages;
@@ -287,6 +298,7 @@ pub fn start_rendering(
invert, invert,
black, black,
white, white,
fit_or_fill,
(area_w, area_h) (area_w, area_h)
) { ) {
// If that fn returned Some, that means it needed to be re-rendered for some // 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 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 // 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 // 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; continue;
} }
@@ -430,6 +442,7 @@ struct RenderedContext {
result_rects: Vec<HighlightRect> result_rects: Vec<HighlightRect>
} }
#[expect(clippy::too_many_arguments)]
fn render_single_page_to_ctx( fn render_single_page_to_ctx(
page: &Page, page: &Page,
search_term: Option<&str>, search_term: Option<&str>,
@@ -437,6 +450,7 @@ fn render_single_page_to_ctx(
invert: bool, invert: bool,
black: i32, black: i32,
white: i32, white: i32,
fit_or_fill: FitOrFill,
(area_w, area_h): (f32, f32) (area_w, area_h): (f32, f32)
) -> Result<RenderedContext, mupdf::error::Error> { ) -> Result<RenderedContext, mupdf::error::Error> {
let result_rects = match prev_render.num_search_found { 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 // then, get the size of the page
let bounds = page.bounds()?; 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 scaled = scale_img_for_area(page_dim, (area_w, area_h), fit_or_fill);
let p_aspect_ratio = p_width / p_height; let ScaledResult {
width: surface_w,
// Then we get the full pixel dimensions of the area provided to us, and the aspect ratio height: surface_h,
// of that area scale_factor
let area_aspect_ratio = area_w / area_h; } = scaled;
// 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 colorspace = Colorspace::device_rgb(); let colorspace = Colorspace::device_rgb();
let matrix = Matrix::new_scale(scale_factor, scale_factor); let matrix = Matrix::new_scale(scale_factor, scale_factor);
+41 -19
View File
@@ -24,6 +24,7 @@ use ratatui::{
use ratatui_image::{FontSize, Image}; use ratatui_image::{FontSize, Image};
use crate::{ use crate::{
FitOrFill,
converter::{ConvertedImage, MaybeTransferred}, converter::{ConvertedImage, MaybeTransferred},
kitty::{KittyDisplay, KittyReadyToDisplay}, kitty::{KittyDisplay, KittyReadyToDisplay},
renderer::{RenderError, fill_default}, renderer::{RenderError, fill_default},
@@ -32,7 +33,7 @@ use crate::{
pub struct Tui { pub struct Tui {
name: String, name: String,
page: usize, pub page: usize,
last_render: LastRender, last_render: LastRender,
bottom_msg: BottomMessage, bottom_msg: BottomMessage,
// we use `prev_msg` to, for example, restore the 'search results' message on the bottom after // 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 r_to_l: bool
} }
#[derive(Default)] #[derive(Default, Debug)]
struct Zoom { struct Zoom {
// just how much 'zoom' you have. Doesn't relate to anything specific yet, except that 0 means // just how much 'zoom' you have. Doesn't relate to anything specific yet, except that 0 means
// it fills the screen (instead of fits) // 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 // 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 // within it is valid; I think it's the best way to represent it
#[derive(Default)] #[derive(Default)]
struct RenderedInfo { pub struct RenderedInfo {
// The image, if it has been rendered by `Converter` to that struct // The image, if it has been rendered by `Converter` to that struct
img: Option<ConvertedImage>, img: Option<ConvertedImage>,
// The number of results for the current search term that have been found on this page. None if // 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); frame.render_widget(Skip::new(true), img_area);
KittyDisplay::NoChange KittyDisplay::NoChange
} else { } 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 // 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 // in the borrow checker. Once `-Zpolonius=next` is stabilized, we can rework this
// to look like what we expect. // to look like what we expect.
@@ -191,27 +192,46 @@ impl Tui {
.as_ref() .as_ref()
.is_some_and(|c| matches!(c, ConvertedImage::Kitty { .. })) .is_some_and(|c| matches!(c, ConvertedImage::Kitty { .. }))
{ {
log::debug!("we're inside, it's kitty");
let Some(ConvertedImage::Kitty { ref mut img, area }) = let Some(ConvertedImage::Kitty { ref mut img, area }) =
self.rendered[self.page].img self.rendered[self.page].img
else { else {
unreachable!() unreachable!()
}; };
let img_width = f32::from(area.width); // Ugh I don't like this logic. I wish we could simplify it.
let img_height = f32::from(area.height); let (cell_width, cell_height) = if area.width >= img_area.width
let available_to_real_width_ratio = f32::from(img_area.width) / img_width; && area.height >= img_area.height
let available_to_real_height_ratio = f32::from(img_area.height) / img_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 { if available_to_real_width_ratio > available_to_real_height_ratio {
(img_width, img_height / available_to_real_width_ratio) (img_width, img_height / available_to_real_width_ratio)
} else { } else {
(img_width / available_to_real_height_ratio, img_height) (img_width / available_to_real_height_ratio, img_height)
}; }
};
let width = (width * f32::from(font_size.0)) as u32; let width = (cell_width * f32::from(font_size.0)) as u32;
let height = (height * f32::from(font_size.1)) 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 { return KittyDisplay::DisplayImages(vec![KittyReadyToDisplay {
img, img,
@@ -314,7 +334,7 @@ impl Tui {
frame.render_widget(Image::new(page_img), img_area); frame.render_widget(Image::new(page_img), img_area);
None None
} }
ConvertedImage::Kitty { img, area } => Some((img, Position { ConvertedImage::Kitty { img, area: _ } => Some((img, Position {
x: img_area.x, x: img_area.x,
y: img_area.y y: img_area.y
})) }))
@@ -609,12 +629,13 @@ impl Tui {
Some(InputAction::Redraw) Some(InputAction::Redraw)
} }
'z' => { 'z' => {
self.zoom = match self.zoom { let (zoom, f_or_f) = match self.zoom {
None => Some(Zoom::default()), None => (Some(Zoom::default()), FitOrFill::Fill),
Some(_) => None Some(_) => (None, FitOrFill::Fit)
}; };
self.zoom = zoom;
self.last_render.rect = Rect::default(); self.last_render.rect = Rect::default();
Some(InputAction::Redraw) Some(InputAction::SwitchRenderZoom(f_or_f))
} }
'L' => { 'L' => {
if let Some(z) = &mut self.zoom { if let Some(z) = &mut self.zoom {
@@ -853,7 +874,8 @@ pub enum InputAction {
Search(String), Search(String),
QuitApp, QuitApp,
Invert, Invert,
Fullscreen Fullscreen,
SwitchRenderZoom(crate::FitOrFill)
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]