use std::sync::atomic::{
AtomicU32,
Ordering,
};
use crate::{
Component,
RenderError,
Rendered,
image::ImageProtocol,
layout::Rect,
renderer::ImageCommand,
};
static NEXT_IMAGE_ID: AtomicU32 = AtomicU32::new(1);
const DEFAULT_CELL_WIDTH_PX: u32 = 10;
const DEFAULT_CELL_HEIGHT_PX: u32 = 20;
const DEFAULT_MAX_COLS: u16 = 40;
const DEFAULT_MAX_ROWS: u16 = 20;
pub struct ImageWidget {
id: u32,
data: Vec<u8>,
mime_type: String,
placeholder: String,
protocol: ImageProtocol,
cols: u16,
rows: u16,
}
impl ImageWidget {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>, placeholder: Option<String>) -> Self {
let mime_type = mime_type.into();
let (cols, rows) = compute_default_size(&data, &mime_type);
Self {
id: NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed),
data,
mime_type,
placeholder: placeholder.unwrap_or_else(|| "[image]".to_string()),
protocol: ImageProtocol::default(),
cols,
rows,
}
}
pub fn with_protocol(mut self, protocol: ImageProtocol) -> Self {
self.protocol = protocol;
self
}
pub fn with_size(mut self, cols: u16, rows: u16) -> Self {
self.cols = cols.max(1);
self.rows = rows.max(1);
self
}
fn placeholder_line(&self, width: u16) -> String {
let target = width as usize;
let placeholder_vw = crate::utils::visible_width(&self.placeholder);
if placeholder_vw >= target {
return crate::utils::truncate_to_width(&self.placeholder, width, "");
}
let pad = target - placeholder_vw;
let mut line = self.placeholder.clone();
line.push_str(&" ".repeat(pad));
line
}
}
impl Component for ImageWidget {
fn render(&self, width: u16) -> Result<Rendered, RenderError> {
let display_cols = self.cols.min(width);
let encoded = match self.protocol {
| ImageProtocol::Kitty => {
crate::image::encode_kitty(self.id, &self.data, display_cols, self.rows)
},
| ImageProtocol::Iterm2 => crate::image::encode_iterm2(&self.data, &self.mime_type),
};
let images = if encoded.is_empty() {
Vec::new()
} else {
vec![ImageCommand {
id: self.id,
data: encoded,
row: 0,
col: 0,
}]
};
let mut lines = Vec::new();
if self.rows > 0 {
lines.push(self.placeholder_line(display_cols));
}
for _ in 1..self.rows {
lines.push(" ".repeat(display_cols as usize));
}
Ok(Rendered {
lines,
cursor: None,
images,
})
}
fn render_rect(&self, rect: Rect) -> Result<Rendered, RenderError> {
let mut rendered = match self.render(rect.width) {
| Ok(r) => r,
| Err(e) => return Err(e),
};
rendered.lines.truncate(rect.height as usize);
Ok(rendered)
}
}
fn compute_default_size(data: &[u8], mime_type: &str) -> (u16, u16) {
let dims = match mime_type {
| "image/png" => crate::image::get_png_dimensions(data),
| "image/jpeg" | "image/jpg" => crate::image::get_jpeg_dimensions(data),
| "image/gif" => crate::image::get_gif_dimensions(data),
| "image/webp" => crate::image::get_webp_dimensions(data),
| _ => None,
};
let (pixel_w, pixel_h) = match dims {
| Some(d) => d,
| None => return (20, 10),
};
if pixel_w == 0 || pixel_h == 0 {
return (20, 10);
}
let max_cols = DEFAULT_MAX_COLS as u32;
let max_rows = DEFAULT_MAX_ROWS as u32;
let cols = (pixel_w / DEFAULT_CELL_WIDTH_PX).clamp(1, max_cols);
let rows = ((pixel_h * cols * DEFAULT_CELL_WIDTH_PX) / (pixel_w * DEFAULT_CELL_HEIGHT_PX))
.clamp(1, max_rows) as u16;
let cols = cols as u16;
(cols, rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_widget_default_size_from_png_dimensions() {
let mut data = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
data.extend_from_slice(&[0; 8]);
data.extend_from_slice(&100u32.to_be_bytes());
data.extend_from_slice(&200u32.to_be_bytes());
let widget = ImageWidget::new(data, "image/png", None);
assert_eq!(widget.cols, 10);
assert_eq!(widget.rows, 10);
}
#[test]
fn image_widget_with_size_override() {
let widget = ImageWidget::new(vec![], "image/png", None).with_size(15, 8);
assert_eq!(widget.cols, 15);
assert_eq!(widget.rows, 8);
}
#[test]
fn image_widget_render_reserves_multiple_lines() {
let widget = ImageWidget::new(vec![0x89, 0x50], "image/png", None).with_size(10, 3);
let rendered = widget.render(80).unwrap();
assert_eq!(rendered.lines.len(), 3);
assert!(rendered.lines[0].starts_with("[image]"));
assert_eq!(crate::utils::visible_width(&rendered.lines[1]), 10);
}
#[test]
fn image_widget_render_rect_clips_to_height() {
let widget = ImageWidget::new(vec![0x89, 0x50], "image/png", None).with_size(5, 5);
let rect = Rect::new(0, 0, 80, 2);
let rendered = widget.render_rect(rect).unwrap();
assert_eq!(rendered.lines.len(), 2);
}
#[test]
fn image_widget_render_includes_kitty_dimensions() {
let widget = ImageWidget::new(vec![0x89, 0x50], "image/png", None).with_size(12, 6);
let rendered = widget.render(80).unwrap();
assert_eq!(rendered.images.len(), 1);
let data = &rendered.images[0].data;
assert!(data.contains("c=12"));
assert!(data.contains("r=6"));
}
}