use ratatui_core::layout::Rect;
use crate::term::image::ImageData;
const MAX_IMAGE_COLS: u16 = 60;
pub trait ImageResolver {
fn resolve(&self, url: &str) -> Option<ImageData>;
}
#[derive(Clone, Debug)]
pub struct MarkdownImage {
pub row: u16,
pub indent: u16,
pub cols: u16,
pub rows: u16,
pub data: ImageData,
pub alt: String,
}
impl MarkdownImage {
pub fn rect(&self, area: Rect) -> Rect {
let x = area.x.saturating_add(self.indent).min(area.right());
let y = area.y.saturating_add(self.row).min(area.bottom());
Rect {
x,
y,
width: self.cols.min(area.right().saturating_sub(x)),
height: self.rows.min(area.bottom().saturating_sub(y)),
}
}
}
pub(super) fn image_cell_size(data: &ImageData, avail: u16) -> (u16, u16) {
let cols = avail.clamp(1, MAX_IMAGE_COLS);
let rows = (cols as u32 * data.pixel_height() / (data.pixel_width().max(1) * 2)).clamp(1, 30);
(cols, rows as u16)
}
#[cfg(test)]
mod tests {
use super::super::testutil::*;
use super::super::{Markdown, to_lines};
use super::*;
use crate::highlight::CodeHighlighter;
use crate::style::{StyleSheet, Theme};
use crate::term::image::{ImageData, ImageLayer, ImageSupport};
use ratatui_core::layout::Rect;
use ratatui_core::style::Modifier;
#[test]
fn image_renders_alt_as_a_marked_placeholder() {
let theme = Theme::default();
let lines = to_lines(
"look:  ok",
60,
&theme,
&StyleSheet::from_theme(&theme),
CodeHighlighter::Plain,
);
let whole: String = lines.iter().map(text).collect::<Vec<_>>().join("\n");
assert!(whole.contains("🖼 a cat"), "expected marked alt: {whole:?}");
assert!(whole.contains("look:") && whole.contains("ok"));
let label = lines
.iter()
.flat_map(|l| &l.spans)
.find(|s| s.content.contains("a cat"))
.expect("alt span");
assert_eq!(label.style.fg, Some(theme.code.link));
assert!(label.style.add_modifier.contains(Modifier::UNDERLINED));
}
#[test]
fn image_without_alt_shows_the_url_so_it_is_never_dropped() {
let out = plain("", 60).join("\n");
assert!(out.contains("🖼 "), "marker present: {out:?}");
assert!(out.contains("https://ex.com/x.png"), "url shown: {out:?}");
}
#[test]
fn resolved_block_image_reserves_rows_and_records_a_placement() {
use crate::testing::render;
let theme = Theme::default();
let layer = ImageLayer::new();
let resolver = StubResolver;
let view = Markdown::new("text before\n\n\n\nafter").images(
&resolver,
ImageSupport::Kitty,
&layer,
);
let _buf = render(&view, 40, 12, &theme);
assert_eq!(layer.len(), 1, "one block image recorded");
}
#[test]
fn unresolved_image_stays_an_inline_placeholder_even_with_images_enabled() {
use crate::testing::render;
let theme = Theme::default();
let layer = ImageLayer::new();
let resolver = StubResolver;
let view =
Markdown::new("").images(&resolver, ImageSupport::Kitty, &layer);
let buf = render(&view, 40, 4, &theme);
let mut whole = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
whole.push_str(buf[(x, y)].symbol());
}
}
assert!(whole.contains("🖼 "), "placeholder marker shown: {whole:?}");
assert!(layer.is_empty(), "declined image records no placement");
}
#[test]
fn block_image_without_graphics_support_shows_alt_fallback() {
use crate::testing::render;
let theme = Theme::default();
let layer = ImageLayer::new();
let resolver = StubResolver;
let view = Markdown::new("").images(&resolver, ImageSupport::None, &layer);
let buf = render(&view, 40, 6, &theme);
let mut whole = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
whole.push_str(buf[(x, y)].symbol());
}
}
assert!(
whole.contains("[image: a cat]"),
"alt fallback painted: {whole:?}"
);
assert!(layer.is_empty(), "no placement without graphics support");
}
#[test]
fn block_image_reserves_height_proportional_to_aspect() {
let wide = ImageData::from_rgba(40, 2, vec![0u8; 40 * 2 * 4]).unwrap();
let tall = ImageData::from_rgba(4, 40, vec![0u8; 4 * 40 * 4]).unwrap();
let (_, wr) = image_cell_size(&wide, 40);
let (_, tr) = image_cell_size(&tall, 40);
assert!(wr < tr, "tall image reserves more rows: {wr} vs {tr}");
assert!(wr >= 1 && tr <= 30, "rows are clamped: {wr}, {tr}");
}
#[test]
fn markdown_image_rect_offsets_by_area_and_row() {
let img = MarkdownImage {
row: 3,
indent: 2,
cols: 10,
rows: 4,
data: ImageData::from_rgba(2, 2, vec![0u8; 16]).unwrap(),
alt: String::new(),
};
let rect = img.rect(Rect::new(5, 1, 40, 20));
assert_eq!((rect.x, rect.y, rect.width, rect.height), (7, 4, 10, 4));
let tight = img.rect(Rect::new(5, 1, 8, 5));
assert_eq!(tight.width, 6, "clamped to area right edge");
}
}