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 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 {
-2
View File
@@ -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(())
}
+44
View File
@@ -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
}
}
+9 -1
View File
@@ -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),
}
},
+23 -25
View File
@@ -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<HighlightRect>
}
#[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<RenderedContext, mupdf::error::Error> {
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);
+41 -19
View File
@@ -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<ConvertedImage>,
// 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)]