Add caching of rendered pages so we don't need to re-render them after changing search term if we already rendered them without any matches

This commit is contained in:
itsjunetime
2024-05-26 15:49:51 -06:00
parent a3ad304d2d
commit 46ca98a12d
2 changed files with 83 additions and 25 deletions
+70 -24
View File
@@ -35,6 +35,20 @@ pub struct ImageData {
pub area: Rect pub area: Rect
} }
#[derive(Default)]
struct PrevRender {
successful: bool,
contained_term: Option<bool>
}
fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
vec.clear();
vec.reserve(size.saturating_sub(vec.len()));
for _ in 0..size {
vec.push(T::default());
}
}
// this function has to be sync (non-async) because the poppler::Document needs to be held during // 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 // 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 // code, so it's !Send and thus can't be held across await points. So we can't call any of the
@@ -80,7 +94,8 @@ pub fn start_rendering(
// `split_at_mut` at 0 initially (which bascially makes `right == rendered && left == []`), // `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, // 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 // then we can split at that page and render at both sides of it
let mut rendered = vec![false; n_pages]; let mut rendered = vec![];
fill_default::<PrevRender>(&mut rendered, n_pages);
let mut start_point = 0; let mut start_point = 0;
// This is kinda a weird way of doing this, but if we get a notification that the area // This is kinda a weird way of doing this, but if we get a notification that the area
@@ -102,7 +117,7 @@ pub fn start_rendering(
// If the new area is smaller, then the same high-quality-rendered images // If the new area is smaller, then the same high-quality-rendered images
// will still look fine, so it's ok to leave it. // will still look fine, so it's ok to leave it.
if bigger { if bigger {
rendered = vec![false; n_pages]; fill_default(&mut rendered, n_pages);
continue 'render_pages; continue 'render_pages;
} }
}, },
@@ -111,10 +126,24 @@ pub fn start_rendering(
continue 'render_pages; continue 'render_pages;
}, },
RenderNotif::Search(term) => { RenderNotif::Search(term) => {
rendered = vec![false; n_pages];
if term.is_empty() { if term.is_empty() {
// If the term is set to nothing, then we don't need to re-render
// the pages wherein there were already no search results. So this
// is a little optimization to allow that.
for page in &mut rendered {
if !page.successful || page.contained_term != Some(true) {
page.successful = false;
}
}
search_term = None; search_term = None;
} else { } else {
// But if the term is set to something new, we need to reset all of
// the 'contained_term' fields so that if they now contain the
// term, we can render them with the term, but if they don't, we
// don't need to re-render and send it over again.
for page in &mut rendered {
page.contained_term = None;
}
search_term = Some(term); search_term = Some(term);
} }
continue 'render_pages; continue 'render_pages;
@@ -135,8 +164,14 @@ pub fn start_rendering(
.map(|(idx, p)| (start_point - (idx + 1), p)) .map(|(idx, p)| (start_point - (idx + 1), p))
); );
// we go through each page
for (num, rendered) in page_iter { for (num, rendered) in page_iter {
if *rendered { // we only want to continue if one of the following is met:
// 1. It failed to render last time (we want to retry)
// 2. The `contained_term` is set to None (representing 'Unknown'), meaning that we
// need to at least check if it contains the current term to see if it needs a
// re-render
if rendered.successful && rendered.contained_term.is_some() {
continue; continue;
} }
@@ -150,20 +185,26 @@ pub fn start_rendering(
// We know this is in range 'cause we're iterating over it // We know this is in range 'cause we're iterating over it
let Some(page) = doc.page(num as i32) else { let Some(page) = doc.page(num as i32) else {
sender.blocking_send(Err(RenderError::Render(format!("Couldn't get page {num} ({}) of doc??", num as i32)))) sender.blocking_send(
Err(RenderError::Render(format!("Couldn't get page {num} ({}) of doc?", num as i32)))
)
.unwrap(); .unwrap();
continue; continue;
}; };
let rendered_with_no_results = rendered.successful && rendered.contained_term == Some(false);
// render the page // render the page
let to_send = render_single_page(page, area, num, &search_term) match render_single_page(page, area, num, &search_term, rendered_with_no_results) {
.map(RenderInfo::Page) // If we've already rendered it just fine and we don't need to render it again,
.map_err(RenderError::Render); // just continue. We're all good
Ok(None) => (),
// then send it over // If that fn returned Some, that means it needed to be re-rendered for some
sender.blocking_send(to_send).unwrap(); // reason or another, so we're sending it here
Ok(Some(img)) => sender.blocking_send(Ok(RenderInfo::Page(img))).unwrap(),
*rendered = true; // And if we got an error, then obviously we need to propagate that
Err(e) => sender.blocking_send(Err(RenderError::Render(e))).unwrap()
}
}; };
// Then once we've rendered all these pages, wait until we get another notification // Then once we've rendered all these pages, wait until we get another notification
// that this doc needs to be reloaded // that this doc needs to be reloaded
@@ -183,8 +224,20 @@ fn render_single_page(
page: Page, page: Page,
area: Rect, area: Rect,
page_num: usize, page_num: usize,
search_term: &Option<String> search_term: &Option<String>,
) -> Result<PageInfo, String> { already_rendered_no_results: bool
) -> Result<Option<PageInfo>, String> {
let mut result_rects = search_term
.as_ref()
.map(|term| page.find_text_with_options(term, FindFlags::DEFAULT | FindFlags::MULTILINE))
.unwrap_or_default();
// If there are no search terms on this page, and we've already rendered it with no search
// terms, then just return none to avoid this computation
if result_rects.is_empty() && already_rendered_no_results {
return Ok(None)
}
// First, get the font size; the number of pixels (width x height) per font character (I // 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. // think; it's at least something like that) on this terminal screen.
let size = crossterm::terminal::window_size() let size = crossterm::terminal::window_size()
@@ -246,11 +299,6 @@ fn render_single_page(
ctx.paint().map_err(|e| format!("Couldn't paint Context: {e}"))?; ctx.paint().map_err(|e| format!("Couldn't paint Context: {e}"))?;
page.render(&ctx); page.render(&ctx);
let mut result_rects = search_term
.as_ref()
.map(|term| page.find_text_with_options(term, FindFlags::DEFAULT | FindFlags::MULTILINE))
.unwrap_or_default();
let num_results = result_rects.len(); let num_results = result_rects.len();
let mut highlight_color = Color::new(); let mut highlight_color = Color::new();
@@ -280,9 +328,7 @@ fn render_single_page(
ctx.target().write_to_png(&mut img_data) ctx.target().write_to_png(&mut img_data)
.map_err(|e| format!("Couldn't write surface to png: {e}"))?; .map_err(|e| format!("Couldn't write surface to png: {e}"))?;
// TODO: Maybe cache which pages had no results with the last search term so we don't have to Ok(Some(PageInfo {
// rerender them when the search term is set to empty and rerenders are requested
Ok(PageInfo {
img_data: ImageData { img_data: ImageData {
data: img_data, data: img_data,
area: Rect { area: Rect {
@@ -293,5 +339,5 @@ fn render_single_page(
}, },
page: page_num, page: page_num,
search_results: num_results search_results: num_results
}) }))
} }
+13 -1
View File
@@ -349,7 +349,14 @@ impl Tui {
KeyCode::Down | KeyCode::Char('j') => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), 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::Left | KeyCode::Char('h') => self.change_page(PageChange::Prev, ChangeAmount::Single),
KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
KeyCode::Esc | KeyCode::Char('q') => Some(InputAction::QuitApp), KeyCode::Esc => match self.bottom_msg {
BottomMessage::Input(_) => {
self.set_bottom_msg(None);
Some(InputAction::Redraw)
},
_ => Some(InputAction::QuitApp)
},
KeyCode::Char('q') => Some(InputAction::QuitApp),
KeyCode::Char('g') => { KeyCode::Char('g') => {
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0)))); self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0))));
Some(InputAction::Redraw) Some(InputAction::Redraw)
@@ -412,6 +419,11 @@ impl Tui {
// data to show // data to show
if !term.is_empty() { if !term.is_empty() {
self.set_bottom_msg(Some(BottomMessage::SearchResults(term.clone()))); self.set_bottom_msg(Some(BottomMessage::SearchResults(term.clone())));
} else {
// else, if it's not empty, we just want to reset the bottom
// area to show the default data; we don't want it to like show
// the data from a previous search
self.set_bottom_msg(Some(BottomMessage::Help));
} }
// Reset all the search results // Reset all the search results