use std::cell::RefCell;
use std::io::{self, Write};
use std::rc::Rc;
use ratatui_core::layout::Rect as BufferRect;
use ratatui_core::terminal::{Terminal, TerminalOptions, Viewport};
use ratatui_crossterm::CrosstermBackend;
use super::present::Screen;
use crate::color::Rgb;
use crate::event::{Event, MouseButton, MouseKind};
use crate::geometry::{Rect, Size};
use crate::graphics::Graphics;
use crate::runtime::{App, Command, Harness};
use crate::style::CellStyle;
use crate::widget::{EventCx, MeasureCx, PaintCx, View, Widget};
use crate::widgets::{Fit, Image, ImageData};
#[derive(Clone, Default)]
pub(super) struct Wire(pub(super) Rc<RefCell<Vec<u8>>>);
impl Write for Wire {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0.borrow_mut().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Clone)]
pub(super) struct Desktop {
pub(super) wallpaper: ImageData,
pub(super) selected: Option<usize>,
pub(super) window: Option<(i32, i32)>,
grab: Option<(i32, i32)>,
}
impl Desktop {
pub(super) fn new(wallpaper: &ImageData, window: Option<(i32, i32)>) -> Self {
Self { wallpaper: wallpaper.clone(), selected: None, window, grab: None }
}
}
#[derive(Clone)]
pub(super) enum Msg {
Select(Option<usize>),
Grab(i32, i32),
Move(i32, i32),
Drop,
Close,
}
pub(super) const WINDOW: (u16, u16) = (44, 14);
pub(super) fn tile(index: usize) -> Rect {
let (column, row) = (i32::try_from(index % 2).unwrap_or(0), i32::try_from(index / 2).unwrap_or(0));
Rect::new(3 + column * 17, 2 + row * 4, 14, 2)
}
impl App for Desktop {
type Msg = Msg;
fn update(&mut self, msg: Msg) -> Command<Msg> {
match msg {
Msg::Select(selected) => self.selected = selected,
Msg::Grab(x, y) => self.grab = Some((x, y)),
Msg::Move(x, y) => self.window = Some((x, y)),
Msg::Drop => self.grab = None,
Msg::Close => self.window = None,
}
Command::none()
}
fn view(&self, ui: &mut View<'_, Msg>) {
ui.add(Floor(self.clone())).fill();
}
}
struct Floor(Desktop);
impl Widget<Msg> for Floor {
fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
available
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
cx.register_hit(area);
Widget::<Msg>::paint(&Image::new(&self.0.wallpaper).fit(Fit::Cover), cx, area);
for index in 0..8 {
let at = tile(index);
let selected = self.0.selected == Some(index);
let ground = if selected { Rgb::new(70, 90, 140) } else { Rgb::new(40, 40, 48) };
cx.clear(at, ground);
let words = CellStyle { fg: Some(Rgb::new(220, 220, 225)), ..Default::default() };
cx.text(at.x + 1, at.y, "Documents", words, at.width - 1);
cx.text(at.x + 1, at.y + 1, "folder", words, at.width - 1);
if selected {
let bar = CellStyle { fg: Some(Rgb::new(120, 160, 255)), ..Default::default() };
cx.text(at.x - 1, at.y, "▌", bar, 1);
cx.text(at.x - 1, at.y + 1, "▌", bar, 1);
}
}
if let Some((x, y)) = self.0.window {
let window = Rect::new(x, y, WINDOW.0, WINDOW.1);
cx.clear(window, Rgb::new(29, 29, 35));
let words = CellStyle { fg: Some(Rgb::new(200, 200, 205)), ..Default::default() };
for (offset, line) in (0..WINDOW.1).zip(["Files", "", "Pictures", "Music", "Notes.txt"].iter().cycle()) {
cx.text(x + 2, y + i32::from(offset), line, words, WINDOW.0 - 4);
}
}
}
fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
let Event::Mouse(mouse) = event else { return false };
let window = self.0.window.map(|(x, y)| Rect::new(x, y, WINDOW.0, WINDOW.1));
match mouse.kind {
MouseKind::Down(MouseButton::Left) => {
cx.capture_pointer();
if let Some(window) = window.filter(|window| window.contains(mouse.x, mouse.y)) {
if mouse.y == window.y && mouse.x == window.right() - 1 {
cx.emit(Msg::Close);
} else if mouse.y == window.y {
cx.emit(Msg::Grab(mouse.x - window.x, mouse.y - window.y));
}
} else {
let hit = (0..8).find(|index| {
let at = tile(*index);
Rect::new(at.x - 1, at.y, at.width + 1, at.height).contains(mouse.x, mouse.y)
});
cx.emit(Msg::Select(hit));
}
}
MouseKind::Drag(MouseButton::Left) => {
if let Some((dx, dy)) = self.0.grab {
cx.emit(Msg::Move(mouse.x - dx, mouse.y - dy));
}
}
MouseKind::Up(MouseButton::Left) => cx.emit(Msg::Drop),
_ => return false,
}
true
}
}
pub(super) fn noisy(width: u32, height: u32) -> ImageData {
let mut state = 0x2545_f491_u32;
let rgb: Vec<u8> = (0..width * height)
.flat_map(|at| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
let grain = i32::from(state.to_be_bytes()[0] % 48) - 24;
let (x, y) = (at % width, at / width);
let base = [x * 200 / width + 30, y * 180 / height + 40, 150 - y * 100 / height];
base.map(|channel| u8::try_from((i32::try_from(channel).unwrap_or(0) + grain).clamp(0, 255)).unwrap_or(0))
})
.collect();
ImageData::from_rgb(width, height, &rgb).expect("pixels")
}
pub(super) fn smooth(width: u32, height: u32) -> ImageData {
let rgb: Vec<u8> = (0..width * height)
.flat_map(|at| {
let (x, y) = (at % width, at / width);
let (dx, dy) = (i64::from(x) - i64::from(width) * 7 / 10, i64::from(y) - i64::from(height) / 3);
let sun = dx * dx + dy * dy < i64::from(height / 8).pow(2);
let sea = y > height * 3 / 5;
let pixel = if sun {
[250, 210, 120]
} else if sea {
[20, 60 + y * 60 / height, 110 + y * 60 / height]
} else {
[90 + y * 100 / height, 140 + y * 60 / height, 220]
};
pixel.map(|channel| u8::try_from(channel.min(255)).unwrap_or(255))
})
.collect();
ImageData::from_rgb(width, height, &rgb).expect("pixels")
}
pub(super) fn screen(size: (u16, u16)) -> (Screen<Wire>, Wire) {
let wire = Wire::default();
let options = TerminalOptions { viewport: Viewport::Fixed(BufferRect::new(0, 0, size.0, size.1)) };
let terminal = Terminal::with_options(CrosstermBackend::new(wire.clone()), options).expect("a terminal");
(Screen::new(terminal), wire)
}
fn bytes<A: App>(h: &mut Harness<A>, screen: &mut Screen<Wire>, wire: &Wire) -> usize {
wire.0.borrow_mut().clear();
h.present_to(screen);
wire.0.borrow().len()
}
struct Costs {
first: usize,
idle: usize,
select: usize,
deselect: usize,
open: usize,
step: usize,
close: usize,
}
pub(super) fn click_icon<A: App>(h: &mut Harness<A>, index: usize) {
let at = tile(index);
h.click(at.x + 3, at.y);
}
fn measure(graphics: Graphics, remote: bool, size: (u16, u16), wallpaper: &ImageData) -> Costs {
let mut h = Harness::new(Desktop::new(wallpaper, None), size.0, size.1);
h.set_graphics(graphics).set_remote(remote);
let (mut screen, wire) = screen(size);
let first = bytes(&mut h, &mut screen, &wire);
let idle = bytes(&mut h, &mut screen, &wire);
click_icon(&mut h, 3);
let select = bytes(&mut h, &mut screen, &wire);
click_icon(&mut h, 4);
let deselect = bytes(&mut h, &mut screen, &wire);
let start = (i32::from(size.0) / 2 - 10, 6);
h.send(Msg::Move(start.0, start.1));
let open = bytes(&mut h, &mut screen, &wire);
let steps = 10;
let mut moved = 0;
h.mouse(MouseKind::Down(MouseButton::Left), start.0 + 5, start.1);
for step in 1..=steps {
h.mouse(MouseKind::Drag(MouseButton::Left), start.0 + 5 - step * 2, start.1 + step % 3);
moved += bytes(&mut h, &mut screen, &wire);
}
h.mouse(MouseKind::Up(MouseButton::Left), start.0 + 5 - steps * 2, start.1 + steps % 3);
bytes(&mut h, &mut screen, &wire);
let window = h.app().window.expect("a window");
h.click(window.0 + i32::from(WINDOW.0) - 1, window.1);
assert!(h.app().window.is_none(), "closed");
let close = bytes(&mut h, &mut screen, &wire);
Costs { first, idle, select, deselect, open, step: moved / usize::try_from(steps).unwrap_or(1), close }
}
#[test]
#[ignore = "a measurement, printed for the design note"]
fn sixel_measure() {
println!(
"| screen | picture | decoded | way | first | idle | select | select another | open window | drag step | close window |"
);
for size in [(100u16, 30u16), (200, 50)] {
for (decoded, per_cell, ways) in [
(
"local",
(10, 20),
[
("half blocks", Graphics::HalfBlock, false),
("sixel, split", Graphics::Sixel, false),
("kitty", Graphics::Kitty, false),
]
.as_slice(),
),
(
"remote",
(2, 4),
[
("half blocks", Graphics::HalfBlock, true),
("sixel, whole", Graphics::Sixel, true),
("sixel, split", Graphics::Sixel, false),
("kitty", Graphics::Kitty, true),
]
.as_slice(),
),
] {
let pixels = (u32::from(size.0) * per_cell.0, u32::from(size.1) * per_cell.1);
for (name, picture) in [("noisy", noisy(pixels.0, pixels.1)), ("smooth", smooth(pixels.0, pixels.1))] {
for (way, graphics, remote) in ways {
let c = measure(*graphics, *remote, size, &picture);
println!(
"| {}×{} | {name} | {decoded} | {way} | {} | {} | {} | {} | {} | {} | {} |",
size.0, size.1, c.first, c.idle, c.select, c.deselect, c.open, c.step, c.close
);
}
}
}
}
}