mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
yay zooming woohoo
This commit is contained in:
+10
-2
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user