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 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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||||
|
|||||||
Reference in New Issue
Block a user