use ratatui_core::buffer::Buffer;
use ratatui_core::style::{Color, Modifier};
use super::resample::{self, Half};
use super::{Fit, Image, ImageData, paint_half};
use crate::color::Rgb;
use crate::geometry::Rect;
use crate::style::to_color;
use crate::widget::PaintCx;
#[derive(Debug, Clone)]
pub(crate) struct Picture {
data: ImageData,
fit: Fit,
area: Rect,
cells: Rect,
source: (f64, f64, f64, f64),
visible: Rect,
marker: Rgb,
ground: Rgb,
}
impl Picture {
pub(crate) fn image(&self) -> u64 {
self.data.id()
}
}
#[derive(Debug, Clone)]
pub(crate) struct PicturePlacement {
pub(crate) data: ImageData,
pub(crate) number: u32,
pub(crate) cells: (u16, u16, u16, u16),
pub(crate) crop: (u32, u32, u32, u32),
}
impl PartialEq for PicturePlacement {
fn eq(&self, other: &Self) -> bool {
self.data.id() == other.data.id()
&& self.number == other.number
&& self.cells == other.cells
&& self.crop == other.crop
}
}
impl Eq for PicturePlacement {}
fn marker(ground: Rgb) -> Rgb {
let far = |channel: u8, offset: u8| if channel < 128 { 255 - offset } else { offset };
Rgb::new(far(ground.r, 3), far(ground.g, 7), far(ground.b, 4))
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Tint {
keep: f32,
add: [f32; 3],
}
impl Tint {
fn between(marker: Rgb, ground: Rgb, fg: Rgb, bg: Rgb) -> Option<Self> {
let pairs =
[(marker.r, ground.r, fg.r, bg.r), (marker.g, ground.g, fg.g, bg.g), (marker.b, ground.b, fg.b, bg.b)];
let keeps = pairs.map(|(m, g, f, b)| (f32::from(f) - f32::from(b)) / (f32::from(m) - f32::from(g)));
let keep = keeps.iter().sum::<f32>() / 3.0;
if !(-0.02..=1.02).contains(&keep) || keeps.iter().any(|each| (each - keep).abs() > 0.03) {
return None;
}
let keep = keep.clamp(0.0, 1.0);
let add = pairs.map(|(_, g, _, b)| f32::from(b) - keep * f32::from(g));
Some(Self { keep, add })
}
fn apply(self, colour: Rgb) -> Rgb {
let channel = |value: u8, add: f32| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let channel = (f32::from(value) * self.keep + add).round().clamp(0.0, 255.0) as u8;
channel
};
Rgb::new(channel(colour.r, self.add[0]), channel(colour.g, self.add[1]), channel(colour.b, self.add[2]))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Fate {
Free,
Tinted(Tint),
Covered,
}
fn fate(cell: &ratatui_core::buffer::Cell, picture: &Picture) -> Fate {
if cell.symbol() != " " || !cell.modifier.is_empty() {
return Fate::Covered;
}
let (Color::Rgb(fr, fg, fb), Color::Rgb(br, bg, bb)) = (cell.fg, cell.bg) else {
return Fate::Covered;
};
let (fg, bg) = (Rgb::new(fr, fg, fb), Rgb::new(br, bg, bb));
if fg == picture.marker && bg == picture.ground {
return Fate::Free;
}
Tint::between(picture.marker, picture.ground, fg, bg).map_or(Fate::Covered, Fate::Tinted)
}
impl Image {
pub(super) fn paint_for_terminal(&self, cx: &mut PaintCx<'_>, area: Rect) {
let resample::OnTerminal { cells: (x, y, width, height), source } =
resample::terminal_cells(&self.data, area.width, area.height, self.fit);
let cells = Rect::new(area.x + i32::from(x), area.y + i32::from(y), width, height);
let visible = cells.intersect(cx.clip);
let ground = cx.color("canvas");
let marker = marker(ground);
let (fg, bg) = (to_color(marker), to_color(ground));
cx.decoration(area);
cx.each_cell_within(cells, |_, _, cell| {
cell.set_symbol(" ");
cell.fg = fg;
cell.bg = bg;
cell.modifier = Modifier::empty();
});
if visible.is_empty() {
return;
}
cx.frame.pictures.push(Picture {
data: self.data.clone(),
fit: self.fit,
area,
cells,
source,
visible,
marker,
ground,
});
}
}
#[derive(Debug, Default)]
pub(crate) struct Halves {
entries: Vec<Worked>,
}
#[derive(Debug)]
struct Worked {
key: (u64, u16, u16, Fit),
cells: Vec<Half>,
used: bool,
}
impl Halves {
fn cells(&mut self, picture: &Picture) -> &[Half] {
let key = (picture.data.id(), picture.area.width, picture.area.height, picture.fit);
let index = match self.entries.iter().position(|entry| entry.key == key) {
Some(index) => index,
None => {
let cells = resample::cells(&picture.data, key.1, key.2, key.3);
self.entries.push(Worked { key, cells, used: false });
self.entries.len() - 1
}
};
let entry = &mut self.entries[index];
entry.used = true;
&entry.cells
}
fn settle(&mut self) {
self.entries.retain(|entry| entry.used);
for entry in &mut self.entries {
entry.used = false;
}
}
}
pub(crate) fn resolve(
buf: &mut Buffer,
pictures: &[Picture],
cache: &mut Halves,
halves: bool,
) -> Vec<PicturePlacement> {
let mut placements: Vec<PicturePlacement> = Vec::new();
let screen = Rect::new(i32::from(buf.area.x), i32::from(buf.area.y), buf.area.width, buf.area.height);
for (index, picture) in pictures.iter().enumerate() {
let above = &pictures[index + 1..];
let visible = picture.visible.intersect(screen);
let mut shown = Vec::new();
let mut dimmed = false;
for y in visible.y..visible.bottom() {
for x in visible.x..visible.right() {
if above.iter().any(|later| later.visible.contains(x, y)) {
continue;
}
let Some(cell) = buf.cell(cell_at(x, y)) else { continue };
match fate(cell, picture) {
Fate::Free => shown.push((x, y, None)),
Fate::Tinted(tint) => {
dimmed = true;
shown.push((x, y, Some(tint)));
}
Fate::Covered => {}
}
}
}
let Some(bounds) = bounds(shown.iter().map(|&(x, y, _)| (x, y))) else { continue };
if !dimmed && shown.len() == usize::from(bounds.width) * usize::from(bounds.height) {
let number = 1 + placements.iter().filter(|placed| placed.data.id() == picture.data.id()).count();
placements.push(PicturePlacement {
data: picture.data.clone(),
number: u32::try_from(number).unwrap_or(u32::MAX),
cells: (
u16::try_from(bounds.x).unwrap_or(0),
u16::try_from(bounds.y).unwrap_or(0),
bounds.width,
bounds.height,
),
crop: crop(picture, bounds),
});
} else if halves {
let cells = cache.cells(picture);
let columns = usize::from(picture.area.width);
for (x, y, tint) in shown {
let (Ok(column), Ok(row)) = (usize::try_from(x - picture.area.x), usize::try_from(y - picture.area.y))
else {
continue;
};
let (Some(half), Some(cell)) = (cells.get(row * columns + column), buf.cell_mut(cell_at(x, y))) else {
continue;
};
let half = match (*half, tint) {
(half, None) => half,
(Half::Empty, Some(_)) => Half::Empty,
(Half::Top(top), Some(tint)) => Half::Top(tint.apply(top)),
(Half::Bottom(bottom), Some(tint)) => Half::Bottom(tint.apply(bottom)),
(Half::Both(top, bottom), Some(tint)) => Half::Both(tint.apply(top), tint.apply(bottom)),
};
paint_half(cell, half);
}
}
}
cache.settle();
placements
}
fn cell_at(x: i32, y: i32) -> (u16, u16) {
(u16::try_from(x).unwrap_or(u16::MAX), u16::try_from(y).unwrap_or(u16::MAX))
}
fn bounds(mut cells: impl Iterator<Item = (i32, i32)>) -> Option<Rect> {
let (x, y) = cells.next()?;
let (mut left, mut top, mut right, mut bottom) = (x, y, x, y);
for (x, y) in cells {
left = left.min(x);
top = top.min(y);
right = right.max(x);
bottom = bottom.max(y);
}
let width = u16::try_from(right - left + 1).ok()?;
let height = u16::try_from(bottom - top + 1).ok()?;
Some(Rect::new(left, top, width, height))
}
fn crop(picture: &Picture, shown: Rect) -> (u32, u32, u32, u32) {
let (sx, sy, sw, sh) = picture.source;
let cells = picture.cells;
let along = |from: i32, to: i32, start: f64, length: f64, cells_start: i32, cells: u16, limit: u32| {
let per_cell = length / f64::from(cells.max(1));
let begin = start + f64::from(from - cells_start) * per_cell;
let end = start + f64::from(to - cells_start) * per_cell;
let first = pixels(begin).min(limit.saturating_sub(1));
let last = pixels(end).clamp(first + 1, limit.max(first + 1));
(first, last - first)
};
let (x, width) = along(shown.x, shown.right(), sx, sw, cells.x, cells.width, picture.data.width());
let (y, height) = along(shown.y, shown.bottom(), sy, sh, cells.y, cells.height, picture.data.height());
(x, y, width, height)
}
fn pixels(length: f64) -> u32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let pixels = length.round().max(0.0) as u32;
pixels
}