use std::collections::HashMap;
use ratatui::layout::{Alignment, Rect};
use ratatui::text::Line;
use ratatui::widgets::{Paragraph, Wrap};
use crate::ImageInfo;
pub use image as img_crate;
pub use ratatui_image::{protocol::Protocol, FontSize, Image, Resize};
pub use ratatui_image::picker::{Picker, ProtocolType};
pub struct ImagePlacement {
pub url: String,
pub line_start: usize,
pub cell_cols: u16,
pub cell_rows: u16,
pub alignment: Option<Alignment>,
}
pub fn fit_cell_size(
img: &img_crate::DynamicImage,
font_size: &FontSize,
max_cols: u16,
max_rows: u16,
) -> (u16, u16) {
if max_cols == 0 || max_rows == 0 || img.width() == 0 || img.height() == 0 {
return (max_cols.max(1), max_rows.max(1));
}
let cell_w = font_size.width as f64;
let cell_h = font_size.height as f64;
let img_cols = (img.width() as f64 / cell_w).ceil();
let img_rows = (img.height() as f64 / cell_h).ceil();
let scale_x = max_cols as f64 / img_cols;
let scale_y = max_rows as f64 / img_rows;
let scale = scale_x.min(scale_y).min(1.0);
let cols = (img_cols * scale).ceil().max(1.0) as u16;
let rows = (img_rows * scale).ceil().max(1.0) as u16;
(cols.min(max_cols), rows.min(max_rows))
}
pub fn make_protocol(
picker: &Picker,
img: &img_crate::DynamicImage,
cell_cols: u16,
cell_rows: u16,
) -> Option<Protocol> {
let size = ratatui::layout::Size::new(cell_cols, cell_rows);
picker
.new_protocol(img.clone(), size, Resize::Fit(None))
.ok()
}
pub fn make_clipped_protocol(
picker: &Picker,
img: &img_crate::DynamicImage,
full_size: ratatui::layout::Size,
visible_size: ratatui::layout::Size,
hidden_top: u16,
hidden_left: u16,
) -> Option<Protocol> {
let (fw, fh) = (
picker.font_size().width as u32,
picker.font_size().height as u32,
);
let fit_w = full_size.width as u32 * fw;
let fit_h = full_size.height as u32 * fh;
let scale = (fit_w as f64 / img.width() as f64)
.min(fit_h as f64 / img.height() as f64)
.min(1.0);
let sw = (img.width() as f64 * scale).round() as u32;
let sh = (img.height() as f64 * scale).round() as u32;
let scaled = img.resize_exact(sw, sh, image::imageops::FilterType::Nearest);
let mut padded = image::RgbaImage::from_pixel(fit_w, fit_h, image::Rgba([0, 0, 0, 0]));
image::imageops::overlay(&mut padded, &scaled, 0, 0);
let vis_pix_w = visible_size.width as u32 * fw;
let vis_pix_h = visible_size.height as u32 * fh;
let x_off = (hidden_left as u32 * fw).min(padded.width().saturating_sub(vis_pix_w));
let y_off = (hidden_top as u32 * fh).min(padded.height().saturating_sub(vis_pix_h));
let padded_dyn: img_crate::DynamicImage = padded.into();
let cropped = padded_dyn.crop_imm(x_off, y_off, vis_pix_w, vis_pix_h);
picker
.new_protocol(cropped, visible_size, Resize::Fit(None))
.ok()
}
pub fn make_scrolled_protocol(
picker: &Picker,
img: &img_crate::DynamicImage,
full_size: ratatui::layout::Size,
visible_size: ratatui::layout::Size,
hidden_top: u16,
) -> Option<Protocol> {
make_clipped_protocol(picker, img, full_size, visible_size, hidden_top, 0)
}
pub fn halfblock_picker() -> Picker {
Picker::halfblocks()
}
pub struct ImageViewport {
pub content: Rect,
pub scroll: u16,
}
pub struct ImageRenderRect {
pub url: String,
pub render_rect: Rect,
pub full_cols: u16,
pub full_rows: u16,
pub hidden_top: u16,
pub hidden_left: u16,
pub alignment: Option<Alignment>,
}
pub fn compute_image_render_rects(
placements: &[ImagePlacement],
lines: &[Line],
viewport: &ImageViewport,
) -> Vec<ImageRenderRect> {
let content_top = viewport.content.y as i32;
let content_bottom = (viewport.content.y + viewport.content.height) as i32;
let content_left = viewport.content.x as i32;
let content_right = (viewport.content.x + viewport.content.width) as i32;
let mut render_rects = Vec::new();
for p in placements {
let visual_y: u16 = if p.line_start == 0 {
0
} else {
let end = p.line_start.min(lines.len());
Paragraph::new(lines[..end].to_vec())
.wrap(Wrap { trim: false })
.line_count(viewport.content.width)
.max(1) as u16
};
let unclipped_y0 = content_top + visual_y as i32 - viewport.scroll as i32;
let unclipped_y1 = unclipped_y0 + p.cell_rows as i32;
let y0 = unclipped_y0.max(content_top);
let y1 = unclipped_y1.min(content_bottom);
if y0 >= y1 {
continue;
}
let visible_rows = (y1 - y0) as u16;
let logical_x = match p.alignment {
Some(Alignment::Center) => {
content_left + (viewport.content.width as i32 / 2) - (p.cell_cols as i32 / 2)
}
Some(Alignment::Right) => {
content_left + viewport.content.width as i32 - p.cell_cols as i32
}
_ => content_left,
};
let x0 = logical_x.max(content_left);
let x1 = (logical_x + p.cell_cols as i32).min(content_right);
if x0 >= x1 {
continue;
}
let visible_cols = (x1 - x0) as u16;
let hidden_top = if unclipped_y0 < content_top {
(content_top - unclipped_y0) as u16
} else {
0
};
let hidden_left = if logical_x < content_left {
(content_left - logical_x) as u16
} else {
0
};
render_rects.push(ImageRenderRect {
url: p.url.clone(),
render_rect: Rect {
x: x0 as u16,
y: y0 as u16,
width: visible_cols,
height: visible_rows,
},
full_cols: p.cell_cols,
full_rows: p.cell_rows,
hidden_top,
hidden_left,
alignment: p.alignment,
});
}
render_rects
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_inline_images(
lines: &mut Vec<Line<'static>>,
images: &[ImageInfo],
cache: &HashMap<String, img_crate::DynamicImage>,
protocol_cache: &mut HashMap<String, Protocol>,
picker: &Picker,
font_size: &FontSize,
max_cols: u16,
max_rows: u16,
) -> Vec<ImagePlacement> {
let mut indexed: Vec<(usize, &ImageInfo)> = images.iter().enumerate().collect();
indexed.sort_by_key(|a| a.1.line_index);
for (_, img) in &indexed {
if cache.contains_key(&img.url) && !protocol_cache.contains_key(&img.url) {
if let Some(dyn_img) = cache.get(&img.url) {
let (cols, rows) = fit_cell_size(dyn_img, font_size, max_cols, max_rows);
if cols > 0 && rows > 0 {
if let Some(protocol) = make_protocol(picker, dyn_img, cols, rows) {
protocol_cache.insert(img.url.clone(), protocol);
}
}
}
}
}
let mut placements = Vec::new();
let mut offset: isize = 0;
let mut cursor: isize = 0;
for (_, img) in &indexed {
let adjusted_line = (img.line_index as isize + offset) as usize;
if !protocol_cache.contains_key(&img.url) {
continue;
}
let Some(dyn_img) = cache.get(&img.url) else {
continue;
};
let (cols, rows) = fit_cell_size(dyn_img, font_size, max_cols, max_rows);
if cols == 0 || rows == 0 {
continue;
}
let insert_at = (adjusted_line as isize).max(cursor) as usize;
if insert_at >= lines.len() {
continue;
}
let alignment = lines[insert_at].alignment;
let empty: Vec<Line<'static>> = (0..rows)
.map(|_| {
let mut l = Line::from("");
l.alignment = alignment;
l
})
.collect();
lines.splice(insert_at..=insert_at, empty);
placements.push(ImagePlacement {
url: img.url.clone(),
line_start: insert_at,
cell_cols: cols,
cell_rows: rows,
alignment,
});
offset += rows as isize - 1;
cursor = insert_at as isize + rows as isize;
}
placements
}