use log::debug;
use std::collections::{HashMap, HashSet};
use std::io;
use std::sync::mpsc;
use super::terminal;
use crate::tile::{DocumentMeta, TilePngs, TiledDocumentCache, VisibleTiles, VisualLine};
#[derive(Clone, Copy)]
pub(super) struct Layout {
pub sidebar_cols: u16,
pub image_col: u16, pub image_cols: u16, pub image_rows: u16, pub status_row: u16, pub cell_w: u16, pub cell_h: u16, }
pub(super) struct ViewState {
pub y_offset: u32, pub img_h: u32, pub vp_w: u32, pub vp_h: u32, pub filename: String,
}
pub(super) fn compute_layout(
term_cols: u16,
term_rows: u16,
pixel_w: u16,
pixel_h: u16,
sidebar_cols: u16,
) -> Layout {
let image_col = sidebar_cols;
let image_cols = term_cols.saturating_sub(sidebar_cols);
let image_rows = term_rows.saturating_sub(1);
let status_row = term_rows.saturating_sub(1);
let cell_w = if term_cols > 0 {
pixel_w / term_cols
} else {
1
};
let cell_h = if term_rows > 0 {
pixel_h / term_rows
} else {
1
};
Layout {
sidebar_cols,
image_col,
image_cols,
image_rows,
status_row,
cell_w,
cell_h,
}
}
pub(super) fn vp_dims(layout: &Layout, img_w: u32, img_h: u32) -> (u32, u32) {
let vp_w = (layout.image_cols as u32 * layout.cell_w as u32).min(img_w);
let vp_h = (layout.image_rows as u32 * layout.cell_h as u32).min(img_h);
(vp_w, vp_h)
}
pub(super) fn visual_line_offset(
visual_lines: &[VisualLine],
max_scroll: u32,
line_num: u32,
) -> u32 {
let idx = (line_num as usize).saturating_sub(1); if idx < visual_lines.len() {
visual_lines[idx.saturating_sub(1)].y_px.min(max_scroll)
} else {
0
}
}
pub(super) struct TileImageIds {
pub content_id: u32,
pub sidebar_id: u32,
}
pub(super) struct LoadedTiles {
pub map: HashMap<usize, TileImageIds>,
next_id: u32,
evict_distance: usize,
}
#[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 LoadedTiles {
pub(super) fn new(evict_distance: usize) -> Self {
Self {
map: HashMap::new(),
next_id: 100, evict_distance,
}
}
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.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 TiledDocumentCache,
idx: usize,
req_tx: &mpsc::Sender<usize>,
res_rx: &mpsc::Receiver<(usize, TilePngs)>,
in_flight: &mut HashSet<usize>,
) -> anyhow::Result<()> {
if let Some(action) = self.plan_load(idx) {
if !cache.contains(idx) {
if in_flight.insert(idx) {
let _ = req_tx.send(idx);
}
while !cache.contains(idx) {
let (i, pngs) = res_rx.recv()?;
in_flight.remove(&i);
cache.insert(i, pngs);
}
}
execute_load(&action, cache.get(idx).unwrap())?;
}
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)?;
}
out.flush()
}
}
fn execute_load(action: &LoadAction, pngs: &crate::tile::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) enum ExitReason {
Quit,
Resize { new_cols: u16, new_rows: u16 },
Reload,
ConfigReload,
Navigate { path: std::path::PathBuf },
GoBack,
}
pub(super) struct PrefetchChannels<'a> {
pub req_tx: &'a mpsc::Sender<usize>,
pub res_rx: &'a mpsc::Receiver<(usize, TilePngs)>,
pub in_flight: &'a mut HashSet<usize>,
}
#[allow(clippy::too_many_arguments)]
pub(super) fn redraw(
meta: &DocumentMeta,
cache: &mut TiledDocumentCache,
loaded: &mut LoadedTiles,
layout: &Layout,
state: &ViewState,
acc_peek: Option<u32>,
flash: Option<&str>,
pf: &mut PrefetchChannels<'_>,
) -> anyhow::Result<()> {
let visible = meta.visible_tiles(state.y_offset, state.vp_h);
match &visible {
VisibleTiles::Single { idx, .. } => {
loaded.ensure_loaded(cache, *idx, pf.req_tx, pf.res_rx, pf.in_flight)?;
}
VisibleTiles::Split {
top_idx, bot_idx, ..
} => {
loaded.ensure_loaded(cache, *top_idx, pf.req_tx, pf.res_rx, pf.in_flight)?;
loaded.ensure_loaded(cache, *bot_idx, pf.req_tx, pf.res_rx, pf.in_flight)?;
}
}
loaded.delete_placements()?;
terminal::place_content_tiles(&visible, loaded, layout, state)?;
terminal::place_sidebar_tiles(&visible, loaded, meta.sidebar_width_px, layout)?;
terminal::draw_status_bar(layout, state, acc_peek, flash)?;
Ok(())
}
pub(super) fn send_prefetch(
tx: &mpsc::Sender<usize>,
meta: &DocumentMeta,
cache: &TiledDocumentCache,
in_flight: &mut HashSet<usize>,
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) && !in_flight.contains(&idx) {
debug!("prefetch: requesting tile {idx} (current={current})");
let _ = tx.send(idx);
in_flight.insert(idx);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_layout_basic() {
let l = compute_layout(80, 24, 1280, 576, 6);
assert_eq!(l.sidebar_cols, 6);
assert_eq!(l.image_col, 6);
assert_eq!(l.image_cols, 74);
assert_eq!(l.image_rows, 23);
assert_eq!(l.status_row, 23);
assert_eq!(l.cell_w, 16); assert_eq!(l.cell_h, 24); }
#[test]
fn compute_layout_zero_cols_no_panic() {
let l = compute_layout(0, 0, 0, 0, 0);
assert_eq!(l.image_cols, 0);
assert_eq!(l.cell_w, 1); assert_eq!(l.cell_h, 1);
}
#[test]
fn visual_line_offset_first_line() {
let vls = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_line_range: None,
md_line_exact: None,
},
VisualLine {
y_pt: 0.0,
y_px: 100,
md_line_range: None,
md_line_exact: None,
},
];
assert_eq!(visual_line_offset(&vls, 1000, 1), 0);
}
#[test]
fn visual_line_offset_middle_line() {
let vls = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_line_range: None,
md_line_exact: None,
},
VisualLine {
y_pt: 0.0,
y_px: 100,
md_line_range: None,
md_line_exact: None,
},
VisualLine {
y_pt: 0.0,
y_px: 200,
md_line_range: None,
md_line_exact: None,
},
];
assert_eq!(visual_line_offset(&vls, 1000, 3), 100);
}
#[test]
fn visual_line_offset_out_of_range() {
let vls = vec![VisualLine {
y_pt: 0.0,
y_px: 0,
md_line_range: None,
md_line_exact: None,
}];
assert_eq!(visual_line_offset(&vls, 1000, 99), 0);
}
#[test]
fn visual_line_offset_clamps_to_max() {
let vls = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_line_range: None,
md_line_exact: None,
},
VisualLine {
y_pt: 0.0,
y_px: 500,
md_line_range: None,
md_line_exact: None,
},
];
assert_eq!(visual_line_offset(&vls, 100, 2), 0);
let vls2 = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_line_range: None,
md_line_exact: None,
},
VisualLine {
y_pt: 0.0,
y_px: 9999,
md_line_range: None,
md_line_exact: None,
},
VisualLine {
y_pt: 0.0,
y_px: 10000,
md_line_range: None,
md_line_exact: None,
},
];
assert_eq!(visual_line_offset(&vls2, 500, 3), 500);
}
#[test]
fn vp_dims_viewport_smaller_than_image() {
let l = compute_layout(80, 24, 1280, 576, 6);
let (vp_w, vp_h) = vp_dims(&l, 2000, 5000);
assert_eq!(vp_w, 1184);
assert_eq!(vp_h, 552);
}
#[test]
fn vp_dims_viewport_larger_than_image() {
let l = compute_layout(80, 24, 1280, 576, 6);
let (vp_w, vp_h) = vp_dims(&l, 100, 200);
assert_eq!(vp_w, 100);
assert_eq!(vp_h, 200);
}
#[test]
fn plan_load_allocates_ids() {
let mut loaded = LoadedTiles::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 = LoadedTiles::new(3);
loaded.plan_load(0); assert!(loaded.plan_load(0).is_none());
}
#[test]
fn plan_load_evicts_distant_tiles() {
let mut loaded = LoadedTiles::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 = LoadedTiles::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);
}
}