use log::debug;
use std::collections::{HashMap, HashSet};
use std::io;
use super::layout::{Layout, ScrollState};
use super::terminal;
use crate::highlight::{HighlightRect, HighlightSpec};
use crate::renderer::{TileRenderer, TileResponse};
use crate::tile::{DocumentMeta, VisibleTiles};
use crate::tile_cache::TileCache;
pub(super) struct TileImageIds {
pub content_id: u32,
pub sidebar_id: u32,
}
pub(super) struct HighlightImages {
pub full_id: u32,
pub p75_id: u32,
pub p50_id: u32,
pub p25_id: u32,
pub active_full_id: u32,
pub active_p75_id: u32,
pub active_p50_id: u32,
pub active_p25_id: u32,
}
impl HighlightImages {
fn all_ids(&self) -> [u32; 8] {
[
self.full_id,
self.p75_id,
self.p50_id,
self.p25_id,
self.active_full_id,
self.active_p75_id,
self.active_p50_id,
self.active_p25_id,
]
}
}
pub(super) struct DisplayState {
pub map: HashMap<usize, TileImageIds>,
next_id: u32,
evict_distance: usize,
overlay_rects: HashMap<usize, Vec<HighlightRect>>,
highlight_images: Option<HighlightImages>,
}
#[cfg_attr(not(test), allow(dead_code))]
pub(super) struct LoadAction {
pub idx: usize,
pub content_id: u32,
pub sidebar_id: u32,
pub evict: Vec<(usize, TileImageIds)>,
}
impl DisplayState {
pub(super) fn new(evict_distance: usize) -> Self {
Self {
map: HashMap::new(),
next_id: 100, evict_distance,
overlay_rects: HashMap::new(),
highlight_images: None,
}
}
pub(super) fn plan_load(&mut self, idx: usize) -> Option<LoadAction> {
if self.map.contains_key(&idx) {
return None;
}
let content_id = self.next_id;
let sidebar_id = self.next_id + 1;
self.next_id += 2;
self.map.insert(
idx,
TileImageIds {
content_id,
sidebar_id,
},
);
let to_evict: Vec<usize> = self
.map
.keys()
.filter(|&&k| (k as isize - idx as isize).unsigned_abs() > self.evict_distance)
.copied()
.collect();
let evict = to_evict
.into_iter()
.filter_map(|k| {
self.overlay_rects.remove(&k);
self.map.remove(&k).map(|ids| (k, ids))
})
.collect();
Some(LoadAction {
idx,
content_id,
sidebar_id,
evict,
})
}
pub(super) fn ensure_loaded(
&mut self,
cache: &mut TileCache,
idx: usize,
rh: &mut ForkHandle<'_>,
) -> anyhow::Result<()> {
if let Some(action) = self.plan_load(idx) {
if !cache.contains(idx) {
if rh.in_flight.insert(idx) {
let _ = rh.renderer.send_render_tile(idx);
}
while !cache.contains(idx) {
match rh.renderer.recv()? {
TileResponse::Tile { idx: i, pngs } => {
rh.in_flight.remove(&i);
cache.insert(i, pngs);
}
TileResponse::Rects { idx: i, rects } => {
self.set_overlay_rects(i, rects);
}
}
}
}
execute_load(&action, cache.get(idx).unwrap())?;
}
Ok(())
}
pub(super) fn set_overlay_rects(&mut self, idx: usize, rects: Vec<HighlightRect>) {
self.overlay_rects.insert(idx, rects);
}
pub(super) fn overlay_rects(&self, idx: usize) -> &[HighlightRect] {
self.overlay_rects.get(&idx).map_or(&[], |v| v.as_slice())
}
pub(super) fn has_overlay(&self, idx: usize) -> bool {
self.overlay_rects.contains_key(&idx)
}
pub(super) fn ensure_highlight_images(&mut self) -> io::Result<&HighlightImages> {
if let Some(ref imgs) = self.highlight_images {
return Ok(imgs);
}
let base = self.next_id;
self.next_id += 8;
use crate::highlight::{PATTERN_HEIGHT, PATTERN_WIDTH};
terminal::send_image(crate::highlight::HIGHLIGHT_PNG, base)?;
for (i, pattern) in [
&crate::highlight::PATTERN_P75,
&crate::highlight::PATTERN_P50,
&crate::highlight::PATTERN_P25,
]
.iter()
.enumerate()
{
terminal::send_raw_image(*pattern, PATTERN_WIDTH, PATTERN_HEIGHT, base + 1 + i as u32)?;
}
terminal::send_image(crate::highlight::HIGHLIGHT_ACTIVE_PNG, base + 4)?;
for (i, pattern) in [
&crate::highlight::PATTERN_ACTIVE_P75,
&crate::highlight::PATTERN_ACTIVE_P50,
&crate::highlight::PATTERN_ACTIVE_P25,
]
.iter()
.enumerate()
{
terminal::send_raw_image(*pattern, PATTERN_WIDTH, PATTERN_HEIGHT, base + 5 + i as u32)?;
}
self.highlight_images = Some(HighlightImages {
full_id: base,
p75_id: base + 1,
p50_id: base + 2,
p25_id: base + 3,
active_full_id: base + 4,
active_p75_id: base + 5,
active_p50_id: base + 6,
active_p25_id: base + 7,
});
Ok(self.highlight_images.as_ref().unwrap())
}
pub(super) fn highlight_images(&self) -> Option<&HighlightImages> {
self.highlight_images.as_ref()
}
pub(super) fn clear_all(&mut self) {
self.map.clear();
self.highlight_images = None;
}
pub(super) fn clear_overlay_state(&mut self) {
self.overlay_rects.clear();
}
pub(super) fn delete_overlay_placements(&self) -> io::Result<()> {
if let Some(imgs) = &self.highlight_images {
delete_placements_for_ids(&imgs.all_ids())?;
}
Ok(())
}
pub(super) fn delete_placements(&self) -> io::Result<()> {
use std::io::Write;
let mut out = std::io::stdout();
for ids in self.map.values() {
write!(out, "\x1b_Ga=d,d=i,i={},q=2\x1b\\", ids.content_id)?;
write!(out, "\x1b_Ga=d,d=i,i={},q=2\x1b\\", ids.sidebar_id)?;
}
if let Some(imgs) = &self.highlight_images {
for id in imgs.all_ids() {
write!(out, "\x1b_Ga=d,d=i,i={id},q=2\x1b\\")?;
}
}
out.flush()
}
}
fn delete_placements_for_ids(ids: &[u32]) -> io::Result<()> {
use std::io::Write;
let mut out = std::io::stdout();
for &id in ids {
write!(out, "\x1b_Ga=d,d=i,i={id},q=2\x1b\\")?;
}
out.flush()
}
fn execute_load(action: &LoadAction, pngs: &crate::tile_cache::TilePngs) -> anyhow::Result<()> {
terminal::send_image(&pngs.content, action.content_id)?;
terminal::send_image(&pngs.sidebar, action.sidebar_id)?;
for (_, ids) in &action.evict {
let _ = terminal::delete_image(ids.content_id);
let _ = terminal::delete_image(ids.sidebar_id);
}
Ok(())
}
pub(super) struct ForkHandle<'a> {
pub renderer: &'a mut TileRenderer,
pub in_flight: &'a mut HashSet<usize>,
}
#[allow(clippy::too_many_arguments)]
pub(super) fn redraw(
meta: &DocumentMeta,
cache: &mut TileCache,
loaded: &mut DisplayState,
layout: &Layout,
scroll: &ScrollState,
filename: &str,
acc_peek: Option<u32>,
flash: Option<&str>,
rh: &mut ForkHandle<'_>,
) -> anyhow::Result<()> {
let visible = meta.visible_tiles(scroll.y_offset, scroll.vp_h);
match &visible {
VisibleTiles::Single { idx, .. } => {
loaded.ensure_loaded(cache, *idx, rh)?;
}
VisibleTiles::Split {
top_idx, bot_idx, ..
} => {
loaded.ensure_loaded(cache, *top_idx, rh)?;
loaded.ensure_loaded(cache, *bot_idx, rh)?;
}
}
loaded.delete_placements()?;
terminal::place_content_tiles(&visible, loaded, layout, scroll)?;
terminal::place_sidebar_tiles(&visible, loaded, meta.sidebar_width_px, layout)?;
terminal::place_overlay_rects(&visible, loaded, layout)?;
terminal::draw_status_bar(layout, scroll, filename, acc_peek, flash)?;
Ok(())
}
pub(super) fn send_prefetch(
rh: &mut ForkHandle<'_>,
meta: &DocumentMeta,
cache: &TileCache,
y_offset: u32,
) {
let current = (y_offset / meta.tile_height_px) as usize;
for idx in [current + 1, current + 2, current.wrapping_sub(1)] {
if idx < meta.tile_count && !cache.contains(idx) && !rh.in_flight.contains(&idx) {
debug!("prefetch: requesting tile {idx} (current={current})");
let _ = rh.renderer.send_render_tile(idx);
rh.in_flight.insert(idx);
}
}
}
pub(super) fn update_overlays(
meta: &DocumentMeta,
loaded: &mut DisplayState,
cache: &mut TileCache,
scroll: &ScrollState,
spec: &HighlightSpec,
rh: &mut ForkHandle<'_>,
) -> anyhow::Result<()> {
let visible = meta.visible_tiles(scroll.y_offset, scroll.vp_h);
let indices: Vec<usize> = match &visible {
VisibleTiles::Single { idx, .. } => vec![*idx],
VisibleTiles::Split {
top_idx, bot_idx, ..
} => vec![*top_idx, *bot_idx],
};
let mut needed = 0;
for &idx in &indices {
if loaded.has_overlay(idx) {
continue;
}
let _ = rh.renderer.send_find_rects(idx, spec);
needed += 1;
}
while needed > 0 {
match rh.renderer.recv()? {
TileResponse::Rects { idx, rects } => {
needed -= 1;
loaded.set_overlay_rects(idx, rects);
}
TileResponse::Tile { idx, pngs } => {
rh.in_flight.remove(&idx);
cache.insert(idx, pngs);
}
}
}
if indices
.iter()
.any(|idx| !loaded.overlay_rects(*idx).is_empty())
{
loaded.ensure_highlight_images()?;
}
Ok(())
}
pub(super) fn drain_responses(
rh: &mut ForkHandle<'_>,
cache: &mut TileCache,
display: &mut DisplayState,
) -> anyhow::Result<()> {
while let Some(resp) = rh.renderer.try_recv()? {
match resp {
TileResponse::Tile { idx, pngs } => {
debug!(
"drain: received tile {idx} ({} + {} bytes)",
pngs.content.len(),
pngs.sidebar.len()
);
rh.in_flight.remove(&idx);
cache.insert(idx, pngs);
}
TileResponse::Rects { idx, rects } => {
display.set_overlay_rects(idx, rects);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn redraw_and_prefetch(
meta: &DocumentMeta,
cache: &mut TileCache,
display: &mut DisplayState,
layout: &Layout,
scroll: &ScrollState,
filename: &str,
acc_peek: Option<u32>,
flash: Option<&str>,
search_spec: Option<&HighlightSpec>,
rh: &mut ForkHandle<'_>,
) -> anyhow::Result<()> {
drain_responses(rh, cache, display)?;
redraw(
meta, cache, display, layout, scroll, filename, acc_peek, flash, rh,
)?;
if let Some(spec) = search_spec {
update_overlays(meta, display, cache, scroll, spec, rh)?;
let visible = meta.visible_tiles(scroll.y_offset, scroll.vp_h);
terminal::place_overlay_rects(&visible, display, layout)?;
}
send_prefetch(rh, meta, cache, scroll.y_offset);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_load_allocates_ids() {
let mut loaded = DisplayState::new(3);
let action = loaded.plan_load(0).unwrap();
assert_eq!(action.idx, 0);
assert_eq!(action.content_id, 100);
assert_eq!(action.sidebar_id, 101);
assert!(action.evict.is_empty());
}
#[test]
fn plan_load_already_loaded_returns_none() {
let mut loaded = DisplayState::new(3);
loaded.plan_load(0); assert!(loaded.plan_load(0).is_none());
}
#[test]
fn plan_load_evicts_distant_tiles() {
let mut loaded = DisplayState::new(2); loaded.plan_load(0);
loaded.plan_load(1);
loaded.plan_load(2);
let action = loaded.plan_load(5).unwrap();
assert!(!action.evict.is_empty());
assert!(action.evict.iter().any(|(idx, _)| *idx == 0));
assert!(!loaded.map.contains_key(&0));
}
#[test]
fn plan_load_increments_ids() {
let mut loaded = DisplayState::new(3);
let a1 = loaded.plan_load(0).unwrap();
let a2 = loaded.plan_load(1).unwrap();
assert_eq!(a1.content_id, 100);
assert_eq!(a1.sidebar_id, 101);
assert_eq!(a2.content_id, 102);
assert_eq!(a2.sidebar_id, 103);
}
#[test]
fn clear_overlay_state_empties_rects() {
let mut loaded = DisplayState::new(3);
loaded.set_overlay_rects(0, vec![]);
assert!(loaded.has_overlay(0));
loaded.clear_overlay_state();
assert!(!loaded.has_overlay(0));
}
}