Initial commit since things seem pretty solid

This commit is contained in:
itsjunetime
2024-05-16 18:23:11 -06:00
commit f298468dc8
10 changed files with 2530 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
use std::{path::PathBuf, str::FromStr};
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
use glib::{LogField, LogLevel, LogWriterOutput};
use notify::{RecursiveMode, Watcher};
use ratatui::{backend::CrosstermBackend, Terminal};
use ratatui_image::picker::Picker;
use tui::{InputAction, Tui};
use futures_util::stream::StreamExt;
use renderer::{RenderInfo, RenderNotif};
mod tui;
mod renderer;
mod skip;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let file = args.next().expect("Program requires a file to process");
let path = PathBuf::from_str(&file)?.canonicalize()?;
let (watch_tx, render_rx) = tokio::sync::mpsc::channel(1);
let tui_tx = watch_tx.clone();
// we need to call this outside the recommended_watcher call because if we call it inside, that
// will be calling it from a thread not owned by the tokio runtime (since it's created by
// calling thread::spawn) and that will cause a panic
let mut watcher = notify::recommended_watcher(move |_| {
// This shouldn't fail to send unless the receiver gets disconnected. If that's happened,
// then like the main thread has panicked or something, so it doesn't matter if this panics
// as well
watch_tx.blocking_send(renderer::RenderNotif::Reload).unwrap();
})?;
// We're making this nonrecursive 'cause we're just watching a single file, so there's nothing
// to recurse into
watcher.watch(&path, RecursiveMode::NonRecursive)?;
let file_path = format!("file://{}", path.clone().into_os_string().to_string_lossy());
let (render_tx, mut tui_rx) = tokio::sync::mpsc::channel(1);
// We need to create `picker` on this thread because if we create it on the `renderer` thread,
// it messes up something with user input. Input never makes it to the crossterm thing
let mut picker = Picker::from_termios()?;
picker.guess_protocol();
// then we want to spawn off the rendering task
// We need to use the thread::spawn API so that this exists in a thread not owned by tokio,
// since the methods we call in `start_rendering` will panic if called in an async context
std::thread::spawn(move || { renderer::start_rendering(file_path, render_tx, render_rx) });
let mut ev_stream = crossterm::event::EventStream::new();
let file_name = path.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_else(|| "Unknown file".into())
.to_string();
let mut tui = tui::Tui::new(file_name, picker);
let backend = CrosstermBackend::new(std::io::stdout());
let mut term = Terminal::new(backend)?;
// poppler has some annoying logging (e.g. if you request a page index out-of-bounds of a
// document's pages, then it will return `None`, but still log to stderr with CRITICAL level),
// so we want to just ignore all logging since this is a tui app.
glib::log_set_writer_func(noop);
execute!(
term.backend_mut(),
EnterAlternateScreen,
crossterm::cursor::Hide
)?;
enable_raw_mode()?;
let mut main_area = tui::Tui::main_layout(&term.get_frame());
tui_tx.send(RenderNotif::Area(main_area[1])).await?;
loop {
let mut needs_redraw;
tokio::select! {
// First we check if we have any keystrokes
Some(ev) = ev_stream.next() => {
// If we can't get user input, just crash.
let ev = ev.expect("Couldn't get any user input");
needs_redraw = match tui.handle_event(ev) {
None => false,
Some(InputAction::Redraw) => true,
Some(InputAction::QuitApp) => break,
Some(InputAction::JumpingToPage(usize)) => {
tui_tx.send(RenderNotif::JumpToPage(usize)).await?;
true
}
};
},
Some(renderer_msg) = tui_rx.recv() => {
match renderer_msg {
Ok(RenderInfo::NumPages(num)) => tui.set_n_pages(num),
Ok(RenderInfo::Page(img, page_num)) => tui.page_ready(img, page_num),
Err(e) => tui.show_error(e)
}
needs_redraw = true;
}
}
let new_area = Tui::main_layout(&term.get_frame());
if new_area != main_area {
main_area = new_area;
tui_tx.send(RenderNotif::Area(main_area[1])).await?;
needs_redraw = true;
}
if needs_redraw {
term.draw(|f| {
tui.render(f, &main_area);
})?;
}
}
execute!(
term.backend_mut(),
LeaveAlternateScreen,
crossterm::cursor::Show
)?;
disable_raw_mode()?;
Ok(())
}
fn noop(_: LogLevel, _: &[LogField<'_>]) -> LogWriterOutput {
LogWriterOutput::Handled
}
+246
View File
@@ -0,0 +1,246 @@
use cairo::{Antialias, Format};
use image::{DynamicImage, ImageFormat};
use itertools::Itertools;
use oxipng::Options;
use poppler::{Document, Page};
use ratatui::layout::Rect;
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
pub enum RenderNotif {
Area(Rect),
JumpToPage(usize),
Reload
}
#[derive(Debug)]
pub enum RenderError {
Doc(glib::Error),
// Don't like storing an error as a string but it needs to be Send to send to the main thread,
// and it's just going to be shown to the user, so whatever
Render(String)
}
pub enum RenderInfo {
NumPages(usize),
Page(DynamicImage, usize)
}
// this function has to be sync (non-async) because the poppler::Document needs to be held during
// most of it, but that's basically just a wrapper around `*c_void` cause it's just a binding to C
// code, so it's !Send and thus can't be held across await points. So we can't call any of the
// async `send` or `recv` methods in this function body, since those create await points. Which
// means we need to call blocking_(send|recv). Those functions panic if called in an async context.
// So here we are.
// Also we just kinda 'unwrap' all of the send/recv calls here 'cause if they return an error, that
// means the other side's disconnected, which means that the main thread has panicked, which means
// we're done.
pub fn start_rendering(
path: String,
sender: Sender<Result<RenderInfo, RenderError>>,
mut receiver: Receiver<RenderNotif>
) {
// first, wait 'til we get told what the current starting area is so that we can set it to
// know what to render to
let mut area;
loop {
if let RenderNotif::Area(r) = receiver.blocking_recv().unwrap() {
area = r;
break;
}
};
'reload: loop {
let doc = match Document::from_file(&path, None) {
Err(e) => {
sender.blocking_send(Err(RenderError::Doc(e))).unwrap();
return;
},
Ok(d) => d
};
let n_pages = doc.n_pages() as usize;
sender.blocking_send(Ok(RenderInfo::NumPages(n_pages))).unwrap();
// We're using this vec of bools to indicate which page numbers have already been rendered,
// to support people jumping to specific pages and having quick rendering results. We
// `split_at_mut` at 0 initially (which bascially makes `right == rendered && left == []`),
// doing basically nothing, but if we get a notification that something has been jumped to,
// then we can split at that page and render at both sides of it
let mut rendered = vec![false; n_pages];
let mut start_point = 0;
// This is kinda a weird way of doing this, but if we get a notification that the area
// changed, we want to start re-rending all of the pages, but we don't want to reload the
// document. If there was a mechanism to say 'start this for-loop over' then I would do
// that, but I don't think such a thing exists, so this is our attempt
'render_pages: loop {
// what we do with a notif is the same regardless of if we're in the middle of
// rendering the list of pages or we're all done
macro_rules! handle_notif {
($notif:ident) => {
match $notif {
RenderNotif::Reload => continue 'reload,
RenderNotif::Area(new_area) => {
let bigger = new_area.width > area.width || new_area.height > area.height;
area = new_area;
// we only want to re-render pages if the new area is greater than the old
// one, 'cause then we might need sharper images to make it all look good.
// If the new area is smaller, then the same high-quality-rendered images
// will still look fine, so it's ok to leave it.
if bigger {
rendered = vec![false; n_pages];
continue 'render_pages;
}
},
RenderNotif::JumpToPage(page) => {
start_point = page;
continue 'render_pages;
}
}
}
}
let (left, right) = rendered.split_at_mut(start_point);
let page_iter = right.iter_mut()
.enumerate()
.map(|(idx, p)| (idx + start_point, p))
.interleave(
left.iter_mut()
.enumerate()
.map(|(idx, p)| (idx - (start_point + 1), p))
);
for (num, rendered) in page_iter {
if *rendered {
continue;
}
// check if we've been told to change the area that we're rendering to,
// or if we're told to rerender
match receiver.try_recv() {
Err(TryRecvError::Disconnected) => panic!("disconnected :("),
Ok(notif) => handle_notif!(notif),
Err(TryRecvError::Empty) => ()
};
// We know this is in range 'cause we're iterating over it
let page = doc.page(num as i32).unwrap();
// render the page
let to_send = render_single_page(page, area)
.and_then(|img_data| match image::load_from_memory_with_format(&img_data, ImageFormat::Png) {
Ok(img) => {
// TODO find some way to do oxipng stuff maybe. Perchance throw them
// all onto a new thread or whatever. idk.
/*let sender_clone = sender.clone();
std::thread::spawn(move || {
let optimized = oxipng::optimize_from_memory(
&img_data,
&Options::default()
).unwrap();
let img = image::load_from_memory_with_format(&optimized, ImageFormat::Png).unwrap();
sender_clone.blocking_send(Ok(RenderInfo::Page(img, num))).unwrap();
});*/
println!("data is {} while img is {}", img_data.len(), img.as_rgb8().unwrap().as_raw().len());
Ok(img)
},
Err(e) => Err(format!("Couldn't create DynamicImage: {e}"))
}).map(|img| RenderInfo::Page(img, num))
.map_err(RenderError::Render);
// then send it over
sender.blocking_send(to_send).unwrap();
*rendered = true;
};
// Then once we've rendered all these pages, wait until we get another notification
// that this doc needs to be reloaded
loop {
// This once returned None despite the main thing being still connected (I think, at
// last), so I'm just being safe here
let Some(msg) = receiver.blocking_recv() else {
return
};
handle_notif!(msg);
}
}
}
}
fn render_single_page(
page: Page,
area: Rect,
//) -> Result<DynamicImage, String> {
) -> Result<Vec<u8>, String> {
// First, get the font size; the number of pixels (width x height) per font character (I
// think; it's at least something like that) on this terminal screen.
let size = crossterm::terminal::window_size()
.map_err(|e| format!("Couldn't get window size: {e}"))?;
let col_h = size.height / size.rows;
let col_w = size.width / size.columns;
// then, get the size of the page
let (p_width, p_height) = page.size();
// 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_full_h = (area.height * col_h) as f64;
let area_full_w = (area.width * col_w) as f64;
let area_aspect_ratio = area_full_w / area_full_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_full_w as f64 / p_width
} else {
area_full_h as f64 / p_height
};
let surface_width = p_width * scale_factor;
let surface_height = p_height * scale_factor;
let surface = cairo::ImageSurface::create(
Format::ARgb32,
// No matter how big you make these arguments, the image will be drawn at the same
// size. So if you make them really big, the image will be drawn on a quarter of it. If
// you make them really small, the image will cover more than all of the surface.
//
// However, that only stands as long as you don't scale the context that you place this
// surface into. If you scale the dimensions of this image by n, then scale the context
// by that same amount, then it'll still fit perfectly into the context, but be
// rendered at higher quality.
surface_width as i32,
surface_height as i32
).map_err(|e| format!("Couldn't create ImageSurface: {e}"))?;
let ctx = cairo::Context::new(surface)
.map_err(|e| format!("Couldn't create Context: {e}"))?;
ctx.scale(scale_factor, scale_factor);
// The default background color of PDFs (at least, I think) is white, so we need to set
// that as the background color, then paint, then render.
ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0);
ctx.set_antialias(Antialias::Best);
ctx.paint().map_err(|e| format!("Couldn't paint Context: {e}"))?;
page.render_for_printing(&ctx);
ctx.scale(1. / scale_factor, 1. / scale_factor);
let mut img_data = Vec::new();
ctx.target().write_to_png(&mut img_data)
.map_err(|e| format!("Couldn't write surface to png: {e}"))?;
/*let img = image::load_from_memory_with_format(&img_data, ImageFormat::Png)
.map_err(|e| format!("Couldn't load image from provided data: {e}"))?;
Ok(img)*/
Ok(img_data)
}
+21
View File
@@ -0,0 +1,21 @@
use ratatui::widgets::Widget;
pub struct Skip {
skip: bool
}
impl Skip {
pub fn new(skip: bool) -> Self {
Self { skip }
}
}
impl Widget for Skip {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
for x in area.x..(area.x + area.width) {
for y in area.y..(area.y + area.height) {
buf.get_mut(x, y).skip = self.skip;
}
}
}
}
+403
View File
@@ -0,0 +1,403 @@
use std::rc::Rc;
use crossterm::event::{Event, KeyCode, MouseEventKind};
use image::DynamicImage;
use ratatui::{layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders, Padding}, Frame};
use ratatui_image::{picker::Picker, protocol::Protocol, Image, Resize};
use crate::{renderer::RenderError, skip::Skip};
pub struct Tui {
name: String,
page: usize,
error: Option<String>,
input_state: Option<InputCommand>,
// Used as a way to track if we need to draw the images, to save ratatui from doing a lot of
// diffing work
last_render: LastRender,
// So we hold the `Picker` here and store the `RenderedImage` as a option between the
// `DynamicImage` and `Box<dyn Protocol>` because the Protocol thing is much less
// space-effecient (since it needs to store like a large string instead of just bytes of data)
// so we want to store them as `DynamicImage`s until they're shown, at which point we render
// them to the `Box<dyn Protocol>` and keep them like that
picker: Picker,
rendered: Vec<Option<RenderedImage>>,
}
#[derive(Default, Debug)]
struct LastRender {
rect: Rect,
pages_shown: usize,
unused_width: u16
}
enum RenderedImage {
Image(DynamicImage),
Text(Box<dyn Protocol>)
}
enum InputCommand {
GoToPage(usize)
}
impl Tui {
pub fn new(name: String, picker: Picker) -> Tui {
Self {
name,
page: 0,
error: None,
input_state: None,
picker,
last_render: LastRender::default(),
rendered: vec![]
}
}
pub fn main_layout(frame: &Frame<'_>) -> Rc<[Rect]> {
Layout::default()
.constraints([
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3)
])
.horizontal_margin(4)
.vertical_margin(2)
.split(frame.size())
}
pub fn render(&mut self, frame: &mut Frame<'_>, main_area: &[Rect]) {
let top_block = Block::new()
.padding(Padding {
right: 2,
left: 2,
..Padding::default()
})
.borders(Borders::BOTTOM);
let top_area = top_block.inner(main_area[0]);
let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len());
let top_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(page_nums_text.len() as u16)
]).split(top_area);
let title = Span::styled(
&self.name,
Style::new()
.fg(Color::Cyan)
);
let page_nums = Span::styled(
&page_nums_text,
Style::new()
.fg(Color::Cyan)
);
frame.render_widget(top_block, main_area[0]);
frame.render_widget(title, top_layout[0]);
frame.render_widget(page_nums, top_layout[1]);
let bottom_block = Block::new()
.padding(Padding {
top: 1,
right: 2,
left: 2,
bottom: 0
})
.borders(Borders::TOP);
let bottom_area = bottom_block.inner(main_area[2]);
frame.render_widget(bottom_block, main_area[2]);
let rendered_str = format!(
"Rendered: {}%",
(self.rendered.iter().filter(|i| i.is_some()).count() * 100) / self.rendered.len()
);
let bottom_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(rendered_str.len() as u16)
]).split(bottom_area);
let rendered_span = Span::styled(
&rendered_str,
Style::new()
.fg(Color::Cyan)
);
frame.render_widget(rendered_span, bottom_layout[1]);
if let Some(ref error_str) = self.error {
let span = Span::styled(
format!("Couldn't render a page: {error_str}"),
Style::new()
.fg(Color::Red)
);
frame.render_widget(span, bottom_layout[0]);
} else if let Some(ref cmd) = self.input_state {
match cmd {
InputCommand::GoToPage(page) => {
let span = Span::styled(
format!("Go to: {page}"),
Style::new()
.fg(Color::Blue)
);
frame.render_widget(span, bottom_layout[0]);
}
}
}
let mut img_area = main_area[1];
let size = frame.size();
if size == self.last_render.rect {
// If we haven't resized (and haven't used the Rect as a way to mark that we need to
// resize this time), then go through every element in the buffer where any Image would
// be written and set to skip it so that ratatui doesn't spend a lot of time diffing it
// each re-render
self.last_render.rect = size;
frame.render_widget(Skip::new(true), img_area);
} else {
// here we calculate how many pages can fit in the available area.
let mut test_area_w = img_area.width;
// go through our pages, starting at the first one we want to view
let page_widths = self.rendered[self.page..].iter()
// and get their indices (I know it's offset, we fix it down below when we actually
// render each page)
.enumerate()
// and only take as many as are ready to be rendered
.take_while(|(_, page)| page.is_some())
// and map it to their width (in cells on the terminal, not pixels)
.flat_map(|(idx, page)|
page.as_ref().map(|img| (
idx,
match img {
RenderedImage::Image(img) => (img.width() / self.picker.font_size.0 as u32) as u16,
RenderedImage::Text(img) => img.rect().width,
}
))
)
// and then take them as long as they won't overflow the available area.
.take_while(|(_, width)| {
match test_area_w.checked_sub(*width) {
Some(new_val) => {
test_area_w = new_val;
true
},
None => false
}
})
.collect::<Vec<_>>();
if page_widths.is_empty() {
// If none are ready to render, just show the loading thing
Self::render_loading_in(frame, img_area)
} else {
let total_width = page_widths
.iter()
.map(|(_, w)| w)
.sum::<u16>();
self.last_render.pages_shown = page_widths.len();
let unused_width = img_area.width - total_width;
self.last_render.unused_width = unused_width;
img_area.x += unused_width / 2;
for (page_idx, width) in page_widths {
// now, theoretically, when we call this, this page should *not* be None, but we do
// have to account for that possibility since we can't `borrow` the image from self
// when passing it in to `render_single_page` since that would be a mutable
// reference + an immutable reference (and also we need to potentially temporarily
// remove it from the array of rendered pages to replace it with a text-rendered
// image)
self.render_single_page(frame, page_idx + self.page, Rect { width, ..img_area });
img_area.x += width;
}
// frame.bypass_diff = true;
// we want to set this at the very end so it doesn't get set somewhere halfway through and
// then the whole diffing thing messes it up
self.last_render.rect = frame.size();
}
}
}
fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) {
match self.rendered[page_idx] {
Some(ref page_img) => {
let dyn_img = match page_img {
RenderedImage::Image(_) => {
// Couldn't think of a better way to do this. We need to `take` the
// image out so we can transform it into a RenderedImage::Text, but we
// don't want to `take` it out when it's already a `Text` or `None`...
// idk, maybe i'll think of something better later.
let Some(RenderedImage::Image(img)) = self.rendered[page_idx].take() else {
unreachable!();
};
// We don't actually want to Crop this image, but we've already
// verified (with the ImageSurface stuff) that the image is the correct
// size for the area given, so to save ratatui the work of having to
// resize it, we tell them to crop it to fit.
let dyn_img = match self.picker.new_protocol(img, img_area, Resize::Crop) {
Ok(img) => img,
Err(e) => {
self.error = Some(format!("Couldn't convert DynamicImage to ratatui image: {e}"));
return;
}
};
self.rendered[page_idx] = Some(RenderedImage::Text(dyn_img));
let Some(RenderedImage::Text(ref txt)) = self.rendered[page_idx] else {
unreachable!();
};
txt
}
RenderedImage::Text(ref img) => img,
};
frame.render_widget(Image::new(&**dyn_img), img_area);
},
None => Self::render_loading_in(frame, img_area)
};
}
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
let loading_str = "Loading...";
let inner_space = Layout::horizontal([
Constraint::Length(loading_str.len() as u16),
]).flex(Flex::Center)
.split(area);
let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan));
frame.render_widget(loading_span, inner_space[0]);
}
fn change_page(&mut self, change: PageChange, amt: ChangeAmount) -> Option<InputAction> {
let diff = match amt {
ChangeAmount::Single => 1,
ChangeAmount::WholeScreen => self.last_render.pages_shown
};
let old = self.page;
match change {
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)),
PageChange::Prev => self.set_page(self.page.saturating_sub(diff)),
}
(old != self.page).then_some(InputAction::Redraw)
}
pub fn set_n_pages(&mut self, n_pages: usize) {
self.rendered = Vec::with_capacity(n_pages);
for _ in 0..n_pages {
self.rendered.push(None);
}
// mark that we need to re-render the images
}
pub fn page_ready(&mut self, img: DynamicImage, page_num: usize) {
// If this new image woulda fit within the available space on the last render AND it's
// within the range where it might've been rendered with the last shown pages, then reset
// the last rect marker so that all images are forced to redraw on next render and this one
// is drawn with them
let img_w = (img.width() / self.picker.font_size.0 as u32) as u16;
if img_w <= self.last_render.unused_width {
let num_fit = self.last_render.unused_width / img_w;
if page_num >= self.page && (self.page + num_fit as usize) >= page_num {
self.last_render.rect = Rect::default();
}
}
// We always just set this here because we handle reloading in the `set_n_pages` function.
// If the document was reloaded, then It'll have the `set_n_pages` called to set the new
// number of pages, so the vec will already be cleared
self.rendered[page_num] = Some(RenderedImage::Image(img));
if page_num > 10 { panic!() }
}
pub fn handle_event(&mut self, ev: Event) -> Option<InputAction> {
match ev {
Event::Key(key) => {
match key.code {
KeyCode::Right | KeyCode::Char('l') => self.change_page(PageChange::Next, ChangeAmount::Single),
KeyCode::Down | KeyCode::Char('j') => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
KeyCode::Left | KeyCode::Char('h') => self.change_page(PageChange::Prev, ChangeAmount::Single),
KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
KeyCode::Esc | KeyCode::Char('q') => Some(InputAction::QuitApp),
KeyCode::Char('g') => {
self.input_state = Some(InputCommand::GoToPage(0));
Some(InputAction::Redraw)
},
KeyCode::Char(c) => {
let Some(InputCommand::GoToPage(ref mut page)) = self.input_state else {
return None;
};
c.to_digit(10)
.map(|input_num| {
*page = (*page * 10) + input_num as usize;
InputAction::Redraw
})
},
KeyCode::Enter => self.input_state.take()
.and_then(|cmd| match cmd {
// Only forward the command if it's within range
InputCommand::GoToPage(page) => (page < self.rendered.len()).then(|| {
self.set_page(page);
InputAction::JumpingToPage(page)
})
}),
_ => None,
}
},
Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::ScrollRight => self.change_page(PageChange::Next, ChangeAmount::Single),
MouseEventKind::ScrollDown => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
MouseEventKind::ScrollLeft => self.change_page(PageChange::Prev, ChangeAmount::Single),
MouseEventKind::ScrollUp => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
_ => None,
}
// One of these options is Event::Resize, and we don't care about that because
// we always check, regardless, if the available area for the images has
// changed.
_ => None,
}
}
pub fn show_error(&mut self, err: RenderError) {
self.error = Some(match err {
RenderError::Doc(e) => format!("Couldn't open document: {e}"),
RenderError::Render(e) => format!("Couldn't render page: {e}")
});
}
fn set_page(&mut self, page: usize) {
if page != self.page {
// mark that we need to re-render the images
self.last_render.rect = Rect::default();
self.page = page;
}
}
}
pub enum InputAction {
Redraw,
JumpingToPage(usize),
QuitApp
}
enum PageChange {
Prev,
Next
}
enum ChangeAmount {
WholeScreen,
Single
}