use std::cell::RefCell;
use std::rc::Rc;
use std::time::Instant;
use rich_art::image::RgbaImage;
use rich_art::picture::{ByteBudget, Pacer, Pixels};
use rich_art::tiles::{Tile, Tiler};
use rich_intuituive::interact::{Button, Key, KeyCode, MouseKind};
use rich_intuituive::prelude::*;
use rich_intuituive::tiles_of;
use crate::app::{Browser, CHROME_ROWS, byte_budget};
use crate::cdp::CELL;
use crate::tab::Command;
pub const NEEDS_KITTY: &str = "The pixel view needs Kitty graphics (kitty, WezTerm, Ghostty, or --images kitty): \
showing the projection.";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ScreenStats {
pub frames: u64,
pub dropped: u64,
pub tiles: u64,
pub bytes: u64,
}
#[derive(Debug)]
pub struct Screen {
tiler: Tiler,
pacer: Pacer,
pub stats: ScreenStats,
}
impl Default for Screen {
fn default() -> Screen {
Screen::new()
}
}
impl Screen {
pub fn new() -> Screen {
Screen {
tiler: Tiler::default(),
pacer: Pacer::new(Pixels::Kitty, 1, 1),
stats: ScreenStats::default(),
}
}
pub fn take(
&mut self,
frame: &RgbaImage,
cols: usize,
rows: usize,
budget: &ByteBudget,
now: Instant,
) -> Option<Vec<usize>> {
self.pacer.resize(cols, rows);
if !self.pacer.due(now, budget, 1.0) {
self.stats.dropped += 1;
return None;
}
let changed = self.tiler.update(frame, cols, rows);
let bytes = self.tiler.estimate(&changed, Pixels::Kitty);
self.pacer.sent(now, Some(bytes.max(1)));
self.stats.frames += 1;
self.stats.tiles += changed.len() as u64;
self.stats.bytes += bytes;
Some(changed)
}
pub fn tiles(&self) -> Vec<Tile> {
self.tiler.tiles().to_vec()
}
}
pub fn rgba(frame: &rich_embed::ScreencastFrame) -> Option<RgbaImage> {
let pixels = &frame.pixels;
let rgb = rich_art::image::RgbImage::from_raw(pixels.width, pixels.height, pixels.rgb.clone())?;
Some(rich_art::image::DynamicImage::ImageRgb8(rgb).to_rgba8())
}
pub fn point(column: f32, row: f32) -> (f64, f64) {
(
f64::from(column.max(0.0)) * CELL.0 as f64,
f64::from(row.max(0.0)) * CELL.1 as f64,
)
}
pub fn passes(key: Key) -> bool {
!key.modifiers.ctrl && !key.modifiers.alt && !matches!(key.code, KeyCode::F(_))
}
pub fn available(b: Browser) -> bool {
b.pixels.get_untracked() == Pixels::Kitty
}
pub fn frame(b: Browser, frame: &rich_embed::ScreencastFrame) {
if b.view.get_untracked() != crate::config::View::Pixels {
return;
}
let Some(image) = rgba(frame) else { return };
let (cols, rows) = b.viewport.get_untracked();
let rows = rows.saturating_sub(CHROME_ROWS);
let budget = byte_budget(b);
let screen = b.screen.get_untracked();
let mut screen = screen.borrow_mut();
if screen
.take(&image, cols.into(), rows.into(), &budget, Instant::now())
.is_some()
{
b.screen_tiles.set(screen.tiles());
b.screen_stats.set(screen.stats);
}
}
pub fn view(b: Browser) -> Node {
tiles_of(move || b.screen_tiles.get(), Pixels::Kitty)
.on_mouse(move |_, mouse| match mouse.kind {
MouseKind::Down(Button::Left) => {
let (x, y) = mouse.precise();
let (x, y) = point(x, y);
b.send(Command::Point { x, y });
true
}
MouseKind::ScrollDown => {
b.send(Command::Wheel(3.0 * CELL.1 as f64));
true
}
MouseKind::ScrollUp => {
b.send(Command::Wheel(-3.0 * CELL.1 as f64));
true
}
_ => false,
})
.flex(1)
.name("pixels")
}
pub fn state() -> Rc<RefCell<Screen>> {
Rc::new(RefCell::new(Screen::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use rich_art::image::Rgba;
use std::time::Duration;
#[test]
fn a_frame_the_budget_has_no_room_for_is_dropped_not_queued() {
let mut screen = Screen::new();
let budget = ByteBudget::new(4 << 20);
let white = RgbaImage::from_pixel(640, 256, Rgba([255, 255, 255, 255]));
let now = Instant::now();
assert_eq!(
screen.take(&white, 80, 16, &budget, now).map(|c| c.len()),
Some(40)
);
assert_eq!(screen.take(&white, 80, 16, &budget, now), None);
assert_eq!(screen.stats.dropped, 1);
let later = now + Duration::from_secs(3);
assert_eq!(screen.take(&white, 80, 16, &budget, later), Some(vec![]));
assert_eq!(screen.stats.frames, 2);
}
#[test]
fn keys_with_ctrl_or_alt_and_the_f_keys_stay_noctilucas() {
let key = |name: &str| Key::parse(name).unwrap();
assert!(passes(key("a")) && passes(key("enter")) && passes(key("shift+tab")));
assert!(!passes(key("ctrl+q")) && !passes(key("alt+x")) && !passes(key("f1")));
assert_eq!(point(2.5, 1.0), (2.5 * CELL.0 as f64, CELL.1 as f64));
}
}