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()
}
fn stretch(&self) -> Stretch {
Stretch { cells: self.cells, source: self.source, size: (self.data.width(), self.data.height()) }
}
}
#[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),
pub(crate) stretch: Stretch,
}
impl PicturePlacement {
#[cfg(test)]
pub(crate) fn whole(data: &ImageData, cells: (u16, u16, u16, u16)) -> Self {
let rect = Rect::new(i32::from(cells.0), i32::from(cells.1), cells.2, cells.3);
let (width, height) = (data.width(), data.height());
let stretch =
Stretch { cells: rect, source: (0.0, 0.0, f64::from(width), f64::from(height)), size: (width, height) };
Self { data: data.clone(), number: 1, cells, crop: (0, 0, width, height), stretch }
}
pub(crate) fn part(&self, cells: Rect) -> PicturePlacement {
PicturePlacement {
data: self.data.clone(),
number: self.number,
cells: (
u16::try_from(cells.x).unwrap_or(0),
u16::try_from(cells.y).unwrap_or(0),
cells.width,
cells.height,
),
crop: self.stretch.crop(cells),
stretch: self.stretch,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Stretch {
cells: Rect,
source: (f64, f64, f64, f64),
size: (u32, u32),
}
impl Stretch {
pub(crate) fn crop(&self, shown: Rect) -> (u32, u32, u32, u32) {
let (sx, sy, sw, sh) = self.source;
let cells = self.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 edge = |cell: i32| pixels(start + f64::from(cell - cells_start) * per_cell);
let first = edge(from).min(limit.saturating_sub(1));
let last = edge(to).clamp(first + 1, limit.max(first + 1));
(first, last - first)
};
let (x, width) = along(shown.x, shown.right(), sx, sw, cells.x, cells.width, self.size.0);
let (y, height) = along(shown.y, shown.bottom(), sy, sh, cells.y, cells.height, self.size.1);
(x, y, width, height)
}
pub(crate) fn key(&self, image: u64) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
image.hash(&mut hasher);
(self.cells.x, self.cells.y, self.cells.width, self.cells.height, self.size).hash(&mut hasher);
let (x, y, w, h) = self.source;
(x.to_bits(), y.to_bits(), w.to_bits(), h.to_bits()).hash(&mut hasher);
hasher.finish()
}
}
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)]
pub(crate) struct Dim {
rect: Rect,
color: Rgb,
amount: f32,
over: usize,
}
impl Dim {
pub(crate) fn new(rect: Rect, color: Rgb, amount: f32, over: usize) -> Self {
Self { rect, color, amount, over }
}
}
fn dims_over(dims: &[Dim], index: usize, x: i32, y: i32) -> impl Iterator<Item = &Dim> + Clone {
dims.iter().filter(move |dim| dim.over > index && dim.rect.contains(x, y))
}
fn dimmed<'a>(colour: Rgb, dims: impl Iterator<Item = &'a Dim>) -> Rgb {
dims.fold(colour, |colour, dim| colour.mix(dim.color, dim.amount))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Fate {
Free,
Dimmed,
Covered,
}
fn fate(cell: &ratatui_core::buffer::Cell, picture: &Picture, dims: &[Dim], index: usize, x: i32, y: i32) -> 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;
}
let over = dims_over(dims, index, x, y);
if over.clone().next().is_some() && dimmed(picture.marker, over.clone()) == fg && dimmed(picture.ground, over) == bg
{
Fate::Dimmed
} else {
Fate::Covered
}
}
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) const MOST_PLACES: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Placing {
Split,
Whole,
}
pub(crate) fn resolve(
buf: &mut Buffer,
pictures: &[Picture],
dims: &[Dim],
cache: &mut Halves,
halves: bool,
placing: Placing,
) -> 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 free = Vec::new();
let mut blended = Vec::new();
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, dims, index, x, y) {
Fate::Free => free.push((x, y)),
Fate::Dimmed => blended.push((x, y)),
Fate::Covered => {}
}
}
}
let whole = !free.is_empty() && free.len() == usize::from(visible.width) * usize::from(visible.height);
let rects = match placing {
Placing::Split => rectangles(&free, MOST_PLACES),
Placing::Whole if whole => Some(vec![visible]),
Placing::Whole => None,
};
let halved = match rects {
Some(rects) => {
let before = placements.iter().filter(|placed| placed.data.id() == picture.data.id()).count();
for (offset, rect) in rects.into_iter().enumerate() {
placements.push(PicturePlacement {
data: picture.data.clone(),
number: u32::try_from(before + offset + 1).unwrap_or(u32::MAX),
cells: (
u16::try_from(rect.x).unwrap_or(0),
u16::try_from(rect.y).unwrap_or(0),
rect.width,
rect.height,
),
crop: crop(picture, rect),
stretch: picture.stretch(),
});
}
blended
}
None => {
blended.extend(free);
blended
}
};
if halves && !halved.is_empty() {
let cells = cache.cells(picture);
let columns = usize::from(picture.area.width);
for (x, y) in halved {
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 over = dims_over(dims, index, x, y);
let half = match *half {
Half::Empty => Half::Empty,
Half::Top(top) => Half::Top(dimmed(top, over)),
Half::Bottom(bottom) => Half::Bottom(dimmed(bottom, over)),
Half::Both(top, bottom) => Half::Both(dimmed(top, over.clone()), dimmed(bottom, over)),
};
paint_half(cell, half);
}
}
}
cache.settle();
placements
}
pub(crate) fn rectangles(cells: &[(i32, i32)], most: usize) -> Option<Vec<Rect>> {
let mut done: Vec<Rect> = Vec::new();
let mut open: Vec<Rect> = Vec::new();
let mut row: Vec<Rect> = Vec::new();
let mut index = 0;
while index < cells.len() {
let (_, y) = cells[index];
row.clear();
while index < cells.len() && cells[index].1 == y {
let (x, _) = cells[index];
match row.last_mut() {
Some(run) if run.right() == x => run.width = run.width.saturating_add(1),
_ => row.push(Rect::new(x, y, 1, 1)),
}
index += 1;
}
let mut next = Vec::with_capacity(row.len());
for run in &row {
let above = open.iter().position(|rect| rect.x == run.x && rect.width == run.width && rect.bottom() == y);
next.push(match above {
Some(at) => {
let mut rect = open.swap_remove(at);
rect.height = rect.height.saturating_add(1);
rect
}
None => *run,
});
}
done.append(&mut open);
open = next;
if done.len() + open.len() > most {
return None;
}
}
done.append(&mut open);
done.sort_by_key(|rect| (rect.y, rect.x));
Some(done)
}
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 crop(picture: &Picture, shown: Rect) -> (u32, u32, u32, u32) {
picture.stretch().crop(shown)
}
fn pixels(length: f64) -> u32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let pixels = length.round().max(0.0) as u32;
pixels
}
#[cfg(test)]
#[path = "kitty_tests.rs"]
mod tests;