diff --git a/src/main.rs b/src/main.rs index dfe823f..0478017 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,9 @@ use clap::Parser; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::{ - event, keyboard, mouse, Alignment, ContentFit, Event, Length, Limits, Subscription, + event, + keyboard::{self, key::Named, Key}, + mouse, Alignment, ContentFit, Event, Length, Limits, Subscription, }; use cosmic::{executor, theme, widget, Application, Element}; use std::collections::hash_map::DefaultHasher; @@ -49,6 +51,8 @@ struct Document { page: usize, page_count: Option, rendered_page: Option, + rendering_page: Option, + prefetching_pages: Vec, error: Option, } @@ -64,6 +68,8 @@ impl Document { page: 0, page_count, rendered_page: None, + rendering_page: None, + prefetching_pages: Vec::new(), error: None, } } @@ -93,7 +99,17 @@ enum Message { ZoomOut, ResetZoom, ModifiersChanged(keyboard::Modifiers), - ScrollZoom(f32), + Scroll(f32), + PdfRendered { + path: PathBuf, + page: usize, + result: Result, + }, + PdfPrefetched { + path: PathBuf, + page: usize, + result: Result, + }, } struct NoctuaApp { @@ -167,9 +183,9 @@ impl Application for NoctuaApp { }; app.refresh_title(); - app.ensure_current_rendered(); + let task = app.ensure_current_rendered(); - (app, Task::none()) + (app, task) } fn update(&mut self, message: Self::Message) -> Task { @@ -187,23 +203,10 @@ impl Application for NoctuaApp { } } Message::PreviousPage => { - if let Some(document) = self.current_document_mut() { - if document.kind == DocumentKind::Pdf && document.page > 0 { - document.page -= 1; - document.rendered_page = None; - } - } + self.previous_pdf_page(); } Message::NextPage => { - if let Some(document) = self.current_document_mut() { - if document.kind == DocumentKind::Pdf { - let page_count = document.page_count.unwrap_or(document.page + 2); - if document.page + 1 < page_count { - document.page += 1; - document.rendered_page = None; - } - } - } + self.next_pdf_page(); } Message::ZoomIn => { self.zoom_in(); @@ -217,21 +220,66 @@ impl Application for NoctuaApp { Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; } - Message::ScrollZoom(delta) => { + Message::Scroll(delta) => { if self.modifiers.control() { if delta > 0.0 { self.zoom_in(); } else if delta < 0.0 { self.zoom_out(); } + } else if delta > 0.0 { + self.previous_pdf_page(); + } else if delta < 0.0 { + self.next_pdf_page(); + } + } + Message::PdfRendered { path, page, result } => { + if let Some(document) = self + .documents + .iter_mut() + .find(|document| document.path == path) + { + if document.rendering_page == Some(page) { + document.rendering_page = None; + } + + if document.page == page { + match result { + Ok(rendered) => { + document.rendered_page = Some(rendered); + document.error = None; + } + Err(err) => { + document.rendered_page = None; + document.error = Some(err); + } + } + } + } + } + Message::PdfPrefetched { path, page, result } => { + if let Some(document) = self + .documents + .iter_mut() + .find(|document| document.path == path) + { + document + .prefetching_pages + .retain(|pending| *pending != page); + + if document.page == page && document.rendered_page.is_none() { + if let Ok(rendered) = result { + document.rendered_page = Some(rendered); + document.error = None; + } + } } } } self.refresh_title(); - self.ensure_current_rendered(); - Task::none() + self.ensure_current_rendered() } fn subscription(&self) -> Subscription { @@ -239,10 +287,18 @@ impl Application for NoctuaApp { Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { Some(Message::ModifiersChanged(modifiers)) } + Event::Keyboard(keyboard::Event::KeyPressed { + key: Key::Named(Named::PageUp | Named::ArrowUp | Named::ArrowLeft), + .. + }) if status == event::Status::Ignored => Some(Message::PreviousPage), + Event::Keyboard(keyboard::Event::KeyPressed { + key: Key::Named(Named::PageDown | Named::ArrowDown | Named::ArrowRight), + .. + }) if status == event::Status::Ignored => Some(Message::NextPage), Event::Mouse(mouse::Event::WheelScrolled { delta }) if status == event::Status::Ignored => { - scroll_delta_y(delta).map(Message::ScrollZoom) + scroll_delta_y(delta).map(Message::Scroll) } _ => None, }) @@ -333,6 +389,29 @@ impl NoctuaApp { self.zoom = (self.zoom / 1.2).max(0.2); } + fn previous_pdf_page(&mut self) { + if let Some(document) = self.current_document_mut() { + if document.kind == DocumentKind::Pdf && document.page > 0 { + document.page -= 1; + document.rendered_page = None; + document.error = None; + } + } + } + + fn next_pdf_page(&mut self) { + if let Some(document) = self.current_document_mut() { + if document.kind == DocumentKind::Pdf { + let page_count = document.page_count.unwrap_or(document.page + 2); + if document.page + 1 < page_count { + document.page += 1; + document.rendered_page = None; + document.error = None; + } + } + } + } + fn current_document(&self) -> Option<&Document> { self.documents.get(self.current) } @@ -349,25 +428,84 @@ impl NoctuaApp { self.core.set_header_title(title); } - fn ensure_current_rendered(&mut self) { - let Some(document) = self.current_document_mut() else { - return; + fn ensure_current_rendered(&mut self) -> Task { + let render_task = { + let Some(document) = self.current_document_mut() else { + return Task::none(); + }; + + if document.kind != DocumentKind::Pdf { + return Task::none(); + } + + if document.rendered_page.is_some() { + return self.ensure_adjacent_rendered(); + } + + if document.rendering_page == Some(document.page) { + return Task::none(); + } + + if let Some(cached) = cached_pdf_page(&document.path, document.page) { + document.rendered_page = Some(cached); + document.error = None; + document.rendering_page = None; + return self.ensure_adjacent_rendered(); + } + + document.rendering_page = Some(document.page); + document.error = None; + + render_pdf_task( + document.path.clone(), + document.page, + |path, page, result| Message::PdfRendered { path, page, result }, + ) }; - if document.kind != DocumentKind::Pdf || document.rendered_page.is_some() { - return; + Task::batch([render_task, self.ensure_adjacent_rendered()]) + } + + fn ensure_adjacent_rendered(&mut self) -> Task { + let Some(document) = self.current_document_mut() else { + return Task::none(); + }; + + if document.kind != DocumentKind::Pdf || document.rendered_page.is_none() { + return Task::none(); } - match render_pdf_page(&document.path, document.page) { - Ok(rendered) => { - document.rendered_page = Some(rendered); - document.error = None; - } - Err(err) => { - document.rendered_page = None; - document.error = Some(err); - } + let mut pages = Vec::with_capacity(2); + if document + .page_count + .is_none_or(|page_count| document.page + 1 < page_count) + { + pages.push(document.page + 1); } + if document.page > 0 { + pages.push(document.page - 1); + } + + let path = document.path.clone(); + let pages = pages + .into_iter() + .filter(|page| document.rendering_page != Some(*page)) + .filter(|page| !document.prefetching_pages.contains(page)) + .filter(|page| cached_pdf_page(&path, *page).is_none()) + .collect::>(); + + document.prefetching_pages.extend(pages.iter().copied()); + + let tasks = pages + .into_iter() + .map(|page| { + render_pdf_task(path.clone(), page, |path, page, result| { + Message::PdfPrefetched { path, page, result } + }) + }) + .collect::>(); + + Task::batch(tasks) } fn document_view(&self, document: &Document) -> Element<'_, Message> { @@ -515,13 +653,42 @@ fn pdf_page_count(path: &Path) -> Option { }) } +fn render_pdf_task( + path: PathBuf, + page: usize, + on_done: impl FnOnce(PathBuf, usize, Result) -> Message + Send + 'static, +) -> Task { + Task::perform( + async move { + let result = render_pdf_page(&path, page); + (path, page, result) + }, + move |(path, page, result)| cosmic::Action::App(on_done(path, page, result)), + ) +} + +fn cached_pdf_page(path: &Path, page: usize) -> Option { + let output = pdf_page_output(path, page); + output.exists().then_some(output) +} + +fn pdf_page_output(path: &Path, page: usize) -> PathBuf { + std::env::temp_dir() + .join(format!( + "noctua-pdf-{}-{}", + path_hash(path), + page.saturating_add(1) + )) + .with_extension("png") +} + fn render_pdf_page(path: &Path, page: usize) -> Result { - let prefix = std::env::temp_dir().join(format!( - "noctua-pdf-{}-{}", - path_hash(path), - page.saturating_add(1) - )); - let output = prefix.with_extension("png"); + if let Some(cached) = cached_pdf_page(path, page) { + return Ok(cached); + } + + let output = pdf_page_output(path, page); + let prefix = output.with_extension(""); let status = Command::new("pdftocairo") .arg("-png") @@ -550,6 +717,18 @@ fn render_pdf_page(path: &Path, page: usize) -> Result { fn path_hash(path: &Path) -> u64 { let mut hasher = DefaultHasher::new(); - path.hash(&mut hasher); + + match path.canonicalize() { + Ok(path) => path.hash(&mut hasher), + Err(_) => path.hash(&mut hasher), + } + + if let Ok(metadata) = path.metadata() { + metadata.len().hash(&mut hasher); + if let Ok(modified) = metadata.modified() { + modified.hash(&mut hasher); + } + } + hasher.finish() }