use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
widgets::{Block, Borders, Paragraph},
Frame,
};
use ratatui_image::{Resize, StatefulImage};
use crate::image_diff::{
center_in_area, fit_dimensions, CachedImage, ImageDiffState, MAX_IMAGE_HEIGHT_ROWS,
IMAGE_BOTTOM_MARGIN, IMAGE_TOP_MARGIN, METADATA_HEIGHT,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImagePanelLayout {
pub image_rect: Rect,
pub metadata_rect: Option<Rect>,
pub inner_area: Rect,
pub full_container_height: u16,
}
impl ImagePanelLayout {
pub fn left_margin(&self) -> u16 {
self.image_rect.x.saturating_sub(self.inner_area.x)
}
pub fn right_margin(&self) -> u16 {
self.inner_area
.right()
.saturating_sub(self.image_rect.right())
}
pub fn is_horizontally_centered(&self) -> bool {
let diff = (self.left_margin() as i32 - self.right_margin() as i32).abs();
diff <= 1
}
pub fn is_within_bounds(&self) -> bool {
self.image_rect.x >= self.inner_area.x
&& self.image_rect.y >= self.inner_area.y
&& self.image_rect.right() <= self.inner_area.right()
&& self.image_rect.bottom() <= self.inner_area.bottom()
}
pub fn bottom_margin(&self) -> u16 {
if let Some(meta) = &self.metadata_rect {
meta.y.saturating_sub(self.image_rect.bottom())
} else {
self.inner_area
.bottom()
.saturating_sub(self.image_rect.bottom())
}
}
}
pub fn calculate_image_panel_layout(
image_dims: (u32, u32),
panel_inner: Rect,
expected_available_height: u16,
font_size: (u16, u16),
) -> ImagePanelLayout {
let (img_w, img_h) = image_dims;
let (display_w, display_h) = fit_dimensions(
img_w,
img_h,
panel_inner.width,
expected_available_height,
font_size,
);
let container_w = display_w;
let container_h = display_h;
let content_needed =
IMAGE_TOP_MARGIN + container_h + IMAGE_BOTTOM_MARGIN + METADATA_HEIGHT;
let has_metadata_space = panel_inner.height >= content_needed;
let container_y = panel_inner.y + IMAGE_TOP_MARGIN;
let max_visible_h = panel_inner
.height
.saturating_sub(IMAGE_TOP_MARGIN + 1);
let clamped_h = container_h.min(max_visible_h);
let final_w = if clamped_h < container_h {
let (clamp_w, _) = fit_dimensions(img_w, img_h, panel_inner.width, clamped_h, font_size);
clamp_w
} else {
container_w
};
let final_x = panel_inner.x + panel_inner.width.saturating_sub(final_w) / 2;
let image_rect = Rect::new(final_x, container_y, final_w, clamped_h);
let metadata_rect = if has_metadata_space {
let metadata_y = container_y + container_h + IMAGE_BOTTOM_MARGIN;
Some(Rect::new(
panel_inner.x,
metadata_y,
panel_inner.width,
METADATA_HEIGHT,
))
} else {
None
};
ImagePanelLayout {
image_rect,
metadata_rect,
inner_area: panel_inner,
full_container_height: container_h,
}
}
const MIN_IMAGE_HEIGHT: u16 = 4;
pub fn render_image_diff(
frame: &mut Frame,
area: Rect,
state: &mut ImageDiffState,
file_path: &str,
expected_available_height: u16,
font_size: (u16, u16),
) -> u16 {
let available_height = area.height.saturating_sub(4);
if available_height < MIN_IMAGE_HEIGHT {
render_compact_placeholder(frame, area, file_path);
return area.height;
}
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
render_image_panel(
frame,
chunks[0],
state.before.as_mut(),
"Before (base)",
true, expected_available_height,
font_size,
);
render_image_panel(
frame,
chunks[1],
state.after.as_mut(),
"After (working)",
false, expected_available_height,
font_size,
);
area.height
}
fn render_image_panel(
frame: &mut Frame,
area: Rect,
image: Option<&mut CachedImage>,
label: &str,
is_before: bool,
expected_available_height: u16,
font_size: (u16, u16),
) {
let border_color = if is_before {
Color::Red
} else {
Color::Green
};
let block = Block::default()
.title(label)
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color));
let inner = block.inner(area);
frame.render_widget(block, area);
match image {
Some(cached) => {
let layout = calculate_image_panel_layout(
(cached.display_width(), cached.display_height()),
inner,
expected_available_height,
font_size,
);
if let Some(ref mut protocol) = cached.protocol {
let image_widget = StatefulImage::new().resize(Resize::Fit(None));
frame.render_stateful_widget(image_widget, layout.image_rect, protocol);
if let Some(Err(_)) = protocol.last_encoding_result() {
render_image_placeholder_box(frame, layout.image_rect, cached, font_size);
}
} else {
render_image_placeholder_box(frame, layout.image_rect, cached, font_size);
}
if let Some(metadata_rect) = layout.metadata_rect {
let metadata = Paragraph::new(cached.metadata_string())
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(metadata, metadata_rect);
}
}
None => {
let msg = if is_before {
"(new file)"
} else {
"(deleted)"
};
let para = Paragraph::new(msg)
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
frame.render_widget(para, inner);
}
}
}
fn render_image_placeholder_box(
frame: &mut Frame,
area: Rect,
cached: &CachedImage,
font_size: (u16, u16),
) {
let (display_w, display_h) = fit_dimensions(
cached.original_width,
cached.original_height,
area.width,
area.height,
font_size,
);
let centered = center_in_area(display_w, display_h, area);
let placeholder = format!(
"{}x{} {}",
cached.original_width, cached.original_height, cached.format_name
);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(centered);
frame.render_widget(block, centered);
if inner.height > 0 && inner.width > 0 {
let para = Paragraph::new(placeholder)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Center);
frame.render_widget(para, inner);
}
}
fn render_compact_placeholder(frame: &mut Frame, area: Rect, file_path: &str) {
let text = format!("[image: {}]", file_path);
let para = Paragraph::new(text)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
frame.render_widget(para, area);
}
pub fn calculate_image_height_for_images(
before: Option<(u32, u32)>,
after: Option<(u32, u32)>,
panel_width: u16,
font_size: (u16, u16),
) -> u16 {
let font_w = font_size.0 as f64;
let font_h = font_size.1 as f64;
let (img_w, img_h) = match (before, after) {
(Some((bw, bh)), Some((aw, ah))) => (bw.max(aw), bh.max(ah)),
(Some((w, h)), None) | (None, Some((w, h))) => (w, h),
(None, None) => return MIN_IMAGE_HEIGHT + 4,
};
let available_cells_w = panel_width.saturating_sub(4) / 2;
if available_cells_w == 0 {
return MIN_IMAGE_HEIGHT + 4;
}
let img_cells_w = img_w as f64 / font_w;
let img_cells_h = img_h as f64 / font_h;
let scale = (available_cells_w as f64 / img_cells_w).min(1.0);
let display_h = (img_cells_h * scale).ceil() as u16;
let total_height = display_h.saturating_add(4);
total_height.clamp(MIN_IMAGE_HEIGHT + 4, MAX_IMAGE_HEIGHT_ROWS)
}
pub fn calculate_image_height(terminal_height: u16) -> u16 {
let ideal = (terminal_height as f32 * 0.4) as u16;
let min_height = MIN_IMAGE_HEIGHT + 4; let max_height = terminal_height.saturating_sub(6);
if max_height < min_height {
return terminal_height.saturating_div(2).max(MIN_IMAGE_HEIGHT);
}
ideal.clamp(min_height, max_height)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_image_height() {
let height = calculate_image_height(24);
assert!(height >= MIN_IMAGE_HEIGHT + 4);
assert!(height <= 24 - 6);
let height = calculate_image_height(80);
assert!(height >= MIN_IMAGE_HEIGHT + 4);
assert!(height <= 80 - 6);
}
const TEST_FONT_SIZE: (u16, u16) = (8, 16);
#[test]
fn test_calculate_image_height_for_images_with_dimensions() {
let height = calculate_image_height_for_images(Some((192, 192)), None, 100, TEST_FONT_SIZE);
assert!(height >= MIN_IMAGE_HEIGHT + 4);
assert!(height < 100); }
#[test]
fn test_calculate_image_height_small_image_no_upscale() {
let height = calculate_image_height_for_images(Some((64, 64)), None, 120, TEST_FONT_SIZE);
assert_eq!(height, MIN_IMAGE_HEIGHT + 4);
}
#[test]
fn test_calculate_image_height_large_image_scales_down() {
let height = calculate_image_height_for_images(Some((1024, 1024)), None, 120, TEST_FONT_SIZE);
assert!(height >= 30); assert!(height <= 40); }
#[test]
fn test_calculate_image_height_for_images_uses_larger_dimension() {
let height_both = calculate_image_height_for_images(Some((100, 100)), Some((200, 200)), 100, TEST_FONT_SIZE);
let height_large_only = calculate_image_height_for_images(None, Some((200, 200)), 100, TEST_FONT_SIZE);
assert_eq!(height_both, height_large_only);
}
#[test]
fn test_calculate_image_height_for_images_no_dimensions() {
let height = calculate_image_height_for_images(None, None, 100, TEST_FONT_SIZE);
assert_eq!(height, MIN_IMAGE_HEIGHT + 4);
}
#[test]
fn test_calculate_image_height_for_images_narrow_panel() {
let height = calculate_image_height_for_images(Some((192, 192)), None, 20, TEST_FONT_SIZE);
assert!(height >= MIN_IMAGE_HEIGHT + 4);
}
#[test]
fn test_calculate_image_height_landscape_image() {
let height = calculate_image_height_for_images(Some((800, 200)), None, 120, TEST_FONT_SIZE);
assert!(height >= MIN_IMAGE_HEIGHT + 4);
assert!(height <= 20); }
#[test]
fn test_calculate_image_height_portrait_image() {
let height = calculate_image_height_for_images(Some((200, 800)), None, 120, TEST_FONT_SIZE);
assert!(height <= MAX_IMAGE_HEIGHT_ROWS);
}
#[test]
fn test_calculate_image_height_respects_max_cap() {
use crate::image_diff::MAX_IMAGE_HEIGHT_ROWS;
let height = calculate_image_height_for_images(Some((64, 4096)), None, 120, TEST_FONT_SIZE);
assert_eq!(height, MAX_IMAGE_HEIGHT_ROWS);
}
#[test]
fn test_image_diff_state_creation() {
let state = ImageDiffState {
before: None,
after: None,
};
assert!(state.before.is_none());
assert!(state.after.is_none());
}
#[test]
fn test_height_calculation_consistent_with_fit_dimensions() {
let test_cases = [
(192, 192, 120), (1024, 1024, 120), (800, 200, 120), (200, 800, 120), (64, 64, 120), ];
for (img_w, img_h, panel_width) in test_cases {
let available_w = (panel_width - 4) / 2;
let calc_total = calculate_image_height_for_images(Some((img_w, img_h)), None, panel_width, TEST_FONT_SIZE);
let calc_image_h = calc_total.saturating_sub(4);
let (_, fit_h) = fit_dimensions(img_w, img_h, available_w, 1000, TEST_FONT_SIZE);
if calc_total < MAX_IMAGE_HEIGHT_ROWS {
assert_eq!(
calc_image_h, fit_h,
"Mismatch for {}x{} in {} panel: calc={}, fit={}",
img_w, img_h, panel_width, calc_image_h, fit_h
);
}
}
}
#[test]
fn test_metadata_hidden_when_insufficient_space() {
assert_eq!(METADATA_HEIGHT, 1, "metadata line should be exactly 1 row");
assert_eq!(IMAGE_TOP_MARGIN, 1, "top margin should be 1 row");
assert_eq!(IMAGE_BOTTOM_MARGIN, 1, "bottom margin should be 1 row");
let total_overhead = IMAGE_TOP_MARGIN + IMAGE_BOTTOM_MARGIN + METADATA_HEIGHT;
assert_eq!(total_overhead, 3, "total overhead should be 3 rows");
let inner_height = 3u16;
let has_metadata_space = inner_height > total_overhead;
assert!(!has_metadata_space, "3 rows should hide metadata");
let inner_height = 4u16;
let has_metadata_space = inner_height > total_overhead;
assert!(has_metadata_space, "4 rows should show metadata");
}
const HIFI_FONT_SIZE: (u16, u16) = (8, 16);
const HALFBLOCK_FONT_SIZE: (u16, u16) = (1, 2);
#[test]
fn test_layout_square_image_is_centered_hifi() {
let inner = Rect::new(1, 1, 80, 20);
let layout = calculate_image_panel_layout((200, 200), inner, 18, HIFI_FONT_SIZE);
assert!(
layout.is_horizontally_centered(),
"Square image should be centered. Left margin: {}, Right margin: {}",
layout.left_margin(),
layout.right_margin()
);
}
#[test]
fn test_layout_landscape_image_is_centered_hifi() {
let inner = Rect::new(0, 0, 80, 20);
let layout = calculate_image_panel_layout((400, 200), inner, 18, HIFI_FONT_SIZE);
assert!(
layout.is_horizontally_centered(),
"Landscape image should be centered. Left: {}, Right: {}",
layout.left_margin(),
layout.right_margin()
);
}
#[test]
fn test_layout_portrait_image_is_centered_hifi() {
let inner = Rect::new(0, 0, 80, 30);
let layout = calculate_image_panel_layout((200, 400), inner, 28, HIFI_FONT_SIZE);
assert!(
layout.is_horizontally_centered(),
"Portrait image should be centered. Left: {}, Right: {}",
layout.left_margin(),
layout.right_margin()
);
}
#[test]
fn test_layout_square_image_is_centered_halfblocks() {
let inner = Rect::new(1, 1, 80, 40);
let layout = calculate_image_panel_layout((200, 200), inner, 38, HALFBLOCK_FONT_SIZE);
assert!(
layout.is_horizontally_centered(),
"Square image (halfblocks) should be centered. Left: {}, Right: {}",
layout.left_margin(),
layout.right_margin()
);
}
#[test]
fn test_layout_landscape_image_is_centered_halfblocks() {
let inner = Rect::new(0, 0, 80, 40);
let layout = calculate_image_panel_layout((400, 200), inner, 38, HALFBLOCK_FONT_SIZE);
assert!(
layout.is_horizontally_centered(),
"Landscape image (halfblocks) should be centered. Left: {}, Right: {}",
layout.left_margin(),
layout.right_margin()
);
}
#[test]
fn test_layout_image_within_bounds_full_view() {
let inner = Rect::new(5, 5, 80, 25);
let layout = calculate_image_panel_layout((400, 300), inner, 23, HIFI_FONT_SIZE);
assert!(
layout.is_within_bounds(),
"Image should be within bounds. Image rect: {:?}, Inner: {:?}",
layout.image_rect,
layout.inner_area
);
}
#[test]
fn test_layout_image_within_bounds_partial_view() {
let inner = Rect::new(0, 0, 80, 10); let layout = calculate_image_panel_layout((400, 300), inner, 23, HIFI_FONT_SIZE);
assert!(
layout.is_within_bounds(),
"Partial view image should be clamped to bounds. Image rect: {:?}, Inner: {:?}",
layout.image_rect,
layout.inner_area
);
}
#[test]
fn test_layout_partial_view_clips_correctly() {
let inner = Rect::new(0, 0, 80, 8); let expected_height = 20; let layout = calculate_image_panel_layout((400, 600), inner, expected_height, HIFI_FONT_SIZE);
let max_visible = inner.height.saturating_sub(IMAGE_TOP_MARGIN + 1);
assert!(
layout.image_rect.height <= max_visible,
"Partial view should clip image. Got height {}, max visible {}",
layout.image_rect.height,
max_visible
);
}
#[test]
fn test_layout_metadata_positioned_correctly() {
let inner = Rect::new(0, 0, 80, 25);
let layout = calculate_image_panel_layout((200, 200), inner, 23, HIFI_FONT_SIZE);
assert!(layout.metadata_rect.is_some(), "Should have metadata rect");
let metadata = layout.metadata_rect.unwrap();
assert!(
metadata.y >= layout.image_rect.bottom(),
"Metadata y ({}) should be at or below image bottom ({})",
metadata.y,
layout.image_rect.bottom()
);
}
#[test]
fn test_layout_no_metadata_when_insufficient_space() {
let inner = Rect::new(0, 0, 80, 4);
let layout = calculate_image_panel_layout((200, 200), inner, 3, HIFI_FONT_SIZE);
assert!(
layout.metadata_rect.is_none(),
"Should not have metadata when space is insufficient"
);
}
#[test]
fn test_layout_bottom_margin_reasonable() {
let inner = Rect::new(0, 0, 80, 25);
let layout = calculate_image_panel_layout((400, 200), inner, 23, HIFI_FONT_SIZE);
if layout.metadata_rect.is_some() {
assert!(
layout.bottom_margin() >= IMAGE_BOTTOM_MARGIN,
"Bottom margin ({}) should be at least IMAGE_BOTTOM_MARGIN ({})",
layout.bottom_margin(),
IMAGE_BOTTOM_MARGIN
);
}
}
#[test]
fn test_layout_very_wide_image() {
let inner = Rect::new(0, 0, 80, 20);
let layout = calculate_image_panel_layout((1000, 100), inner, 18, HIFI_FONT_SIZE);
assert!(layout.is_horizontally_centered());
assert!(layout.is_within_bounds());
}
#[test]
fn test_layout_very_tall_image() {
let inner = Rect::new(0, 0, 80, 30);
let layout = calculate_image_panel_layout((100, 1000), inner, 28, HIFI_FONT_SIZE);
assert!(layout.is_horizontally_centered());
assert!(layout.is_within_bounds());
}
#[test]
fn test_layout_tiny_image_no_upscale() {
let inner = Rect::new(0, 0, 80, 20);
let layout = calculate_image_panel_layout((16, 16), inner, 18, HIFI_FONT_SIZE);
assert!(
layout.is_horizontally_centered(),
"Tiny image should be centered"
);
assert!(
layout.image_rect.width <= 80,
"Tiny image should not upscale to fill width"
);
}
#[test]
fn test_layout_image_at_panel_origin() {
let inner = Rect::new(10, 5, 60, 20);
let layout = calculate_image_panel_layout((200, 200), inner, 18, HIFI_FONT_SIZE);
assert!(
layout.image_rect.x >= inner.x,
"Image x should be >= panel x"
);
assert!(
layout.image_rect.y >= inner.y,
"Image y should be >= panel y"
);
assert!(layout.is_within_bounds());
}
#[test]
fn test_layout_margins_symmetric_for_centered_image() {
let test_cases = [
((200, 200), 80, "square"),
((400, 200), 80, "landscape"),
((200, 400), 80, "portrait"),
((100, 100), 60, "small square"),
((800, 400), 100, "wide landscape"),
];
for ((w, h), panel_width, desc) in test_cases {
let inner = Rect::new(0, 0, panel_width, 25);
let layout = calculate_image_panel_layout((w, h), inner, 23, HIFI_FONT_SIZE);
let margin_diff = (layout.left_margin() as i32 - layout.right_margin() as i32).abs();
assert!(
margin_diff <= 1,
"{}: margins should be symmetric. Left: {}, Right: {}, Diff: {}",
desc,
layout.left_margin(),
layout.right_margin(),
margin_diff
);
}
}
#[test]
fn test_layout_partial_views_remain_centered() {
let full_inner = Rect::new(0, 0, 80, 20);
let partial_inner = Rect::new(0, 0, 80, 8);
let expected_height = 18;
let full_layout =
calculate_image_panel_layout((400, 300), full_inner, expected_height, HIFI_FONT_SIZE);
let partial_layout = calculate_image_panel_layout(
(400, 300),
partial_inner,
expected_height,
HIFI_FONT_SIZE,
);
assert!(
full_layout.is_horizontally_centered(),
"Full view should be centered. Left: {}, Right: {}",
full_layout.left_margin(),
full_layout.right_margin()
);
assert!(
partial_layout.is_horizontally_centered(),
"Partial view should be centered. Left: {}, Right: {}",
partial_layout.left_margin(),
partial_layout.right_margin()
);
}
#[test]
fn test_layout_real_world_logo_dimensions() {
let inner = Rect::new(0, 0, 77, 18);
let h_layout =
calculate_image_panel_layout((1500, 650), inner, 16, HIFI_FONT_SIZE);
assert!(
h_layout.is_horizontally_centered(),
"Horizontal logo should be centered. Left: {}, Right: {}",
h_layout.left_margin(),
h_layout.right_margin()
);
let v_layout =
calculate_image_panel_layout((1500, 1392), inner, 16, HIFI_FONT_SIZE);
assert!(
v_layout.is_horizontally_centered(),
"Vertical logo should be centered. Left: {}, Right: {}",
v_layout.left_margin(),
v_layout.right_margin()
);
}
#[test]
fn test_halfblocks_picker_font_size() {
use ratatui_image::picker::Picker;
let picker = Picker::halfblocks();
let font_size = picker.font_size();
assert!(font_size.0 > 0, "Font width should be positive");
assert!(font_size.1 > 0, "Font height should be positive");
let aspect = font_size.1 as f64 / font_size.0 as f64;
assert!(
aspect >= 1.5 && aspect <= 3.0,
"Font aspect ratio {:?} should be roughly 1:2",
font_size
);
}
#[test]
fn test_layout_width_matches_fit_dimensions_when_height_constrained() {
let inner = Rect::new(0, 0, 80, 20);
let expected_height = 18;
let (img_w, img_h) = (400, 800);
let (display_w, display_h) =
fit_dimensions(img_w, img_h, inner.width, expected_height, HIFI_FONT_SIZE);
let layout =
calculate_image_panel_layout((img_w, img_h), inner, expected_height, HIFI_FONT_SIZE);
if display_h >= expected_height {
let (font_w, font_h) = (HIFI_FONT_SIZE.0 as f64, HIFI_FONT_SIZE.1 as f64);
let cell_aspect = (img_w as f64 / font_w) / (img_h as f64 / font_h);
let expected_w = (display_h as f64 * cell_aspect).ceil() as u16;
assert_eq!(
layout.image_rect.width,
expected_w.min(inner.width),
"Width should be calculated from aspect ratio when height constrained"
);
} else {
assert_eq!(
layout.image_rect.width, display_w,
"Width should equal fit_dimensions result when width constrained"
);
}
}
#[test]
fn test_fit_dimensions_matches_ratatui_image_size_for() {
use image::{DynamicImage, RgbaImage};
use ratatui_image::picker::Picker;
use ratatui_image::Resize;
let picker = Picker::halfblocks();
let font_size = picker.font_size();
let test_cases = [
(1500, 650, 77, 30, "horizontal logo"),
(1500, 1392, 77, 30, "vertical logo"),
(800, 400, 80, 20, "landscape"),
(400, 800, 80, 20, "portrait"),
(200, 200, 60, 15, "square"),
];
for (img_w, img_h, panel_w, panel_h, desc) in test_cases {
let img = DynamicImage::ImageRgba8(RgbaImage::new(img_w, img_h));
let protocol = picker.new_resize_protocol(img);
let area = Rect::new(0, 0, panel_w, panel_h);
let ratatui_size = protocol.size_for(Resize::Fit(None), area);
let (our_w, our_h) = fit_dimensions(img_w, img_h, panel_w, panel_h, font_size);
assert_eq!(
(our_w, our_h),
(ratatui_size.width, ratatui_size.height),
"{}: Our fit_dimensions ({}, {}) doesn't match ratatui-image's size_for ({}, {}). \
Image: {}x{}, Panel: {}x{}, Font: {:?}",
desc, our_w, our_h, ratatui_size.width, ratatui_size.height,
img_w, img_h, panel_w, panel_h, font_size
);
}
}
#[test]
fn test_ratatui_image_with_oversized_rect() {
use image::{DynamicImage, RgbaImage};
use ratatui_image::picker::Picker;
use ratatui_image::Resize;
let picker = Picker::halfblocks();
let font_size = picker.font_size();
eprintln!("Halfblocks font_size: {:?}", font_size);
let img = DynamicImage::ImageRgba8(RgbaImage::new(1500, 650));
let protocol = picker.new_resize_protocol(img);
let full_panel = Rect::new(0, 0, 77, 30);
let actual_size = protocol.size_for(Resize::Fit(None), full_panel);
eprintln!("Actual image size for 77x30 panel: {:?}", actual_size);
let (our_w, our_h) = fit_dimensions(1500, 650, 77, 30, font_size);
eprintln!("Our fit_dimensions: ({}, {})", our_w, our_h);
let oversized_rect = Rect::new(10, 5, 77, 30); let size_for_oversized = protocol.size_for(Resize::Fit(None), oversized_rect);
eprintln!("Size for oversized rect: {:?}", size_for_oversized);
assert_eq!(
(our_w, our_h),
(actual_size.width, actual_size.height),
"Our calculation should match ratatui-image's"
);
}
#[test]
fn test_debug_full_layout_calculation() {
let img_dims = (1500u32, 650u32);
let panel_inner = Rect::new(1, 1, 77, 17); let expected_available_height = 15u16; let font_size = (9u16, 18u16);
eprintln!("\n=== Debug Layout Calculation ===");
eprintln!("Image: {}x{}", img_dims.0, img_dims.1);
eprintln!("Panel inner: {:?}", panel_inner);
eprintln!("Expected available height: {}", expected_available_height);
eprintln!("Font size: {:?}", font_size);
let (display_w, display_h) = fit_dimensions(
img_dims.0, img_dims.1,
panel_inner.width,
expected_available_height,
font_size,
);
eprintln!("\nStep 1 - fit_dimensions: ({}, {})", display_w, display_h);
let layout = calculate_image_panel_layout(
img_dims,
panel_inner,
expected_available_height,
font_size,
);
eprintln!("\nStep 2 - Layout result:");
eprintln!(" image_rect: {:?}", layout.image_rect);
eprintln!(" left_margin: {}", layout.left_margin());
eprintln!(" right_margin: {}", layout.right_margin());
eprintln!(" is_centered: {}", layout.is_horizontally_centered());
use image::{DynamicImage, RgbaImage};
use ratatui_image::protocol::{ImageSource, StatefulProtocol, StatefulProtocolType};
use ratatui_image::protocol::kitty::StatefulKitty;
use ratatui_image::Resize;
let img = DynamicImage::ImageRgba8(RgbaImage::new(img_dims.0, img_dims.1));
let source = ImageSource::new(img, font_size, image::Rgba([0, 0, 0, 0]));
let protocol = StatefulProtocol::new(
source,
font_size,
StatefulProtocolType::Kitty(StatefulKitty::new(12345, false)),
);
let ratatui_size = protocol.size_for(Resize::Fit(None), layout.image_rect);
eprintln!("\nStep 3 - ratatui-image size_for({:?}):", layout.image_rect);
eprintln!(" Result: {:?}", ratatui_size);
eprintln!("\n=== Critical Comparison ===");
eprintln!("layout.image_rect.width: {}", layout.image_rect.width);
eprintln!("ratatui_size.width: {}", ratatui_size.width);
eprintln!("MATCH: {}", layout.image_rect.width == ratatui_size.width);
assert_eq!(
layout.image_rect.width, ratatui_size.width,
"Image rect width should match what ratatui-image will actually render"
);
}
#[test]
fn test_fit_dimensions_various_font_sizes() {
use image::{DynamicImage, RgbaImage};
use ratatui_image::protocol::{ImageSource, StatefulProtocol, StatefulProtocolType};
use ratatui_image::protocol::kitty::StatefulKitty;
use ratatui_image::Resize;
let font_sizes = [
(8, 16, "typical 8x16"),
(9, 18, "typical 9x18"),
(10, 20, "typical 10x20"),
(7, 14, "smaller 7x14"),
(12, 24, "larger 12x24"),
];
let img_dims = [
(1500u32, 650u32, "horizontal logo"),
(1500, 1392, "vertical logo"),
];
let panel = (77u16, 16u16);
for (font_w, font_h, font_desc) in font_sizes {
let font_size = (font_w, font_h);
for (img_w, img_h, img_desc) in img_dims {
let img = DynamicImage::ImageRgba8(RgbaImage::new(img_w, img_h));
let source = ImageSource::new(img.clone(), font_size, image::Rgba([0, 0, 0, 0]));
let protocol = StatefulProtocol::new(
source,
font_size,
StatefulProtocolType::Kitty(StatefulKitty::new(12345, false)),
);
let area = Rect::new(0, 0, panel.0, panel.1);
let ratatui_size = protocol.size_for(Resize::Fit(None), area);
let (our_w, our_h) = fit_dimensions(img_w, img_h, panel.0, panel.1, font_size);
eprintln!(
"Font {:?} ({}), Image {}x{} ({}): ratatui=({}, {}), ours=({}, {})",
font_size, font_desc, img_w, img_h, img_desc,
ratatui_size.width, ratatui_size.height, our_w, our_h
);
assert_eq!(
(our_w, our_h),
(ratatui_size.width, ratatui_size.height),
"Font {:?} ({}), Image {} ({}x{}): mismatch",
font_size, font_desc, img_desc, img_w, img_h
);
}
}
}
#[test]
fn test_layout_must_use_display_dimensions_not_original() {
use crate::image_diff::MAX_CACHE_DIMENSION;
use image::{DynamicImage, RgbaImage};
use ratatui_image::protocol::{ImageSource, StatefulProtocol, StatefulProtocolType};
use ratatui_image::protocol::kitty::StatefulKitty;
use ratatui_image::Resize;
let font_size = (16u16, 35u16);
let original_w = 1500u32;
let original_h = 650u32;
let (display_w, display_h) = if original_w > MAX_CACHE_DIMENSION
|| original_h > MAX_CACHE_DIMENSION
{
let scale = MAX_CACHE_DIMENSION as f64 / original_w.max(original_h) as f64;
(
(original_w as f64 * scale) as u32,
(original_h as f64 * scale) as u32,
)
} else {
(original_w, original_h)
};
assert!(
display_w < original_w || display_h < original_h,
"Test requires image to be downscaled. Original: {}x{}, Display: {}x{}",
original_w, original_h, display_w, display_h
);
let display_image = DynamicImage::ImageRgba8(RgbaImage::new(display_w, display_h));
let source = ImageSource::new(display_image, font_size, image::Rgba([0, 0, 0, 0]));
let protocol = StatefulProtocol::new(
source,
font_size,
StatefulProtocolType::Kitty(StatefulKitty::new(12345, false)),
);
let panel_inner = Rect::new(0, 0, 122, 20);
let expected_height = 18u16;
let correct_layout = calculate_image_panel_layout(
(display_w, display_h),
panel_inner,
expected_height,
font_size,
);
let ratatui_size = protocol.size_for(Resize::Fit(None), correct_layout.image_rect);
let buggy_layout = calculate_image_panel_layout(
(original_w, original_h),
panel_inner,
expected_height,
font_size,
);
assert_eq!(
correct_layout.image_rect.width,
ratatui_size.width,
"Layout with display dimensions ({}, {}) should match ratatui-image render width. \
Got layout_w={}, ratatui_w={}",
display_w, display_h,
correct_layout.image_rect.width, ratatui_size.width
);
let width_difference =
buggy_layout.image_rect.width as i32 - correct_layout.image_rect.width as i32;
assert!(
width_difference != 0,
"Layout with original dimensions ({}, {}) should differ from display dimensions. \
Both gave width={}, which means downscaling had no effect on layout (unexpected).",
original_w, original_h,
buggy_layout.image_rect.width
);
assert!(
width_difference.abs() > 5,
"Bug should cause significant width mismatch for centering. \
Original dims gave width={}, display dims gave width={}, diff={}",
buggy_layout.image_rect.width,
correct_layout.image_rect.width,
width_difference
);
}
}