mod data;
mod kitty;
mod resample;
#[cfg(test)]
mod tests;
pub use data::{ImageData, ImageError};
pub(crate) use kitty::{Halves, Picture, PicturePlacement, resolve};
use resample::Half;
use super::EmptyState;
use crate::color::ColorDepth;
use crate::env::Env;
use crate::geometry::{Rect, Size};
use crate::graphics::Graphics;
use crate::i18n::translate_active;
use crate::icons::GlyphMode;
use crate::style::to_color;
use crate::widget::{MeasureCx, PaintCx, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Fit {
#[default]
Contain,
Cover,
Center,
}
#[derive(Debug, Clone)]
pub struct Image {
data: ImageData,
fit: Fit,
}
impl Image {
#[must_use]
pub fn new(data: &ImageData) -> Self {
Self { data: data.clone(), fit: Fit::default() }
}
#[must_use]
pub fn fit(mut self, fit: Fit) -> Self {
self.fit = fit;
self
}
fn cannot_show(&self) -> EmptyState<()> {
let title = self.data.name().map_or_else(|| translate_active("quvyta.image.untitled", &[]), str::to_owned);
let (width, height) = self.data.original_size();
let size = [("width", width.into()), ("height", height.into())];
let message = match self.data.kind() {
Some(kind) => {
let [width, height] = size;
translate_active("quvyta.image.cannot-show-kind", &[("kind", kind.label().into()), width, height])
}
None => translate_active("quvyta.image.cannot-show", &size),
};
EmptyState::new(title).icon("file-image").message(message)
}
}
pub(crate) fn can_draw(env: &Env) -> bool {
env.depth() != ColorDepth::Ansi16 && env.glyph_mode() != GlyphMode::Ascii
}
#[derive(Default)]
struct Cells {
key: Option<(u64, u16, u16, Fit)>,
cells: Vec<Half>,
}
#[cfg(test)]
thread_local! {
static RESAMPLES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
fn resamples() -> u32 {
RESAMPLES.with(std::cell::Cell::get)
}
impl<Msg: 'static> Widget<Msg> for Image {
fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
if cx.env().graphics() != Graphics::Kitty && !can_draw(cx.env()) {
return Widget::<()>::measure(&self.cannot_show(), cx, available);
}
let (width, height) = resample::measure(&self.data, (available.width, available.height), self.fit);
Size::new(width, height)
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
if area.is_empty() {
return;
}
if cx.env().graphics() == Graphics::Kitty {
self.paint_for_terminal(cx, area);
return;
}
if !can_draw(cx.env()) {
Widget::<()>::paint(&self.cannot_show(), cx, area);
return;
}
let key = (self.data.id(), area.width, area.height, self.fit);
let memory = cx.memory::<Cells>();
if memory.key != Some(key) {
memory.cells = resample::cells(&self.data, area.width, area.height, self.fit);
memory.key = Some(key);
#[cfg(test)]
RESAMPLES.with(|count| count.set(count.get() + 1));
}
let cells = std::mem::take(&mut memory.cells);
cx.decoration(area);
let columns = usize::from(area.width);
cx.each_cell_within(area, |column, row, cell| {
if let Some(half) = cells.get(usize::from(row) * columns + usize::from(column)) {
paint_half(cell, *half);
}
});
cx.memory::<Cells>().cells = cells;
}
}
fn paint_half(cell: &mut ratatui_core::buffer::Cell, half: Half) {
let (symbol, fg, bg) = match half {
Half::Empty => return,
Half::Top(top) => ("▀", top, None),
Half::Bottom(bottom) => ("▄", bottom, None),
Half::Both(top, bottom) => ("▀", top, Some(bottom)),
};
cell.set_symbol(symbol);
cell.fg = to_color(fg);
if let Some(bg) = bg {
cell.bg = to_color(bg);
}
cell.modifier = ratatui_core::style::Modifier::empty();
}