use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::{Block, BorderType, Borders, Widget};
use ratatui_image::Image;
use ratatui_image::protocol::Protocol;
use crate::theme::Theme;
pub struct ImagePane<'a, T: Theme> {
theme: &'a T,
protocol: &'a Protocol,
title: Option<&'a str>,
bordered: bool,
}
impl<'a, T: Theme> ImagePane<'a, T> {
pub fn new(theme: &'a T, protocol: &'a Protocol) -> Self {
Self {
theme,
protocol,
title: None,
bordered: true,
}
}
#[must_use]
pub fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
#[must_use]
pub fn bordered(mut self, bordered: bool) -> Self {
self.bordered = bordered;
self
}
}
impl<T: Theme> Widget for ImagePane<'_, T> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
debug_assert!(
self.bordered || self.title.is_none(),
"ImagePane: title is only rendered when bordered(true); the supplied title would be silently dropped",
);
let inner = if self.bordered {
let mut block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(self.theme.border_focused())
.style(self.theme.base());
if let Some(title) = self.title {
block = block.title(Line::styled(format!(" {title} "), self.theme.title()));
}
let inner = block.inner(area);
block.render(area, buf);
inner
} else {
area
};
if inner.width == 0 || inner.height == 0 {
return;
}
Image::new(self.protocol).render(inner, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::EddaCraftTheme;
use ratatui::layout::Rect;
use ratatui_image::Resize;
use ratatui_image::picker::Picker;
fn tiny_protocol(area: Rect) -> Protocol {
let mut img = image::RgbImage::new(2, 2);
img.put_pixel(0, 0, image::Rgb([255, 0, 0]));
img.put_pixel(1, 1, image::Rgb([0, 255, 0]));
let dynamic = image::DynamicImage::ImageRgb8(img);
let picker = Picker::halfblocks();
picker
.new_protocol(dynamic, area, Resize::Fit(None))
.expect("protocol")
}
#[test]
fn renders_border_around_image() {
let theme = EddaCraftTheme;
let area = Rect::new(0, 0, 10, 6);
let proto = tiny_protocol(Rect::new(0, 0, 8, 4));
let mut buf = Buffer::empty(area);
ImagePane::new(&theme, &proto).render(area, &mut buf);
assert_eq!(buf[(0, 0)].symbol(), "╭");
assert_eq!(buf[(9, 5)].symbol(), "╯");
}
#[test]
fn title_appears_on_top_border() {
let theme = EddaCraftTheme;
let area = Rect::new(0, 0, 14, 6);
let proto = tiny_protocol(Rect::new(0, 0, 12, 4));
let mut buf = Buffer::empty(area);
ImagePane::new(&theme, &proto)
.title("Logo")
.render(area, &mut buf);
let top: String = (0..14).map(|x| buf[(x, 0)].symbol().to_string()).collect();
assert!(top.contains("Logo"), "got: {top:?}");
}
#[test]
fn bordered_false_skips_border() {
let theme = EddaCraftTheme;
let area = Rect::new(0, 0, 10, 6);
let proto = tiny_protocol(area);
let mut buf = Buffer::empty(area);
ImagePane::new(&theme, &proto)
.bordered(false)
.render(area, &mut buf);
assert_ne!(buf[(0, 0)].symbol(), "╭");
}
#[test]
fn zero_area_is_a_noop() {
let theme = EddaCraftTheme;
let area = Rect::new(0, 0, 0, 0);
let proto = tiny_protocol(Rect::new(0, 0, 4, 4));
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 5));
ImagePane::new(&theme, &proto).render(area, &mut buf);
}
}