use std::borrow::Cow;
use std::sync::mpsc::{self, TrySendError};
use std::time::{Duration, Instant};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use tui::layout::Rect;
use all_is_cubes::camera::{Camera, StandardCameras, Viewport};
use all_is_cubes::cgmath::{Point2, Vector2};
use all_is_cubes::listen::{ListenableCell, ListenableSource};
use all_is_cubes::math::Rgba;
use all_is_cubes::raytracer::{CharacterBuf, CharacterRtData, ColorBuf, PixelBuf, RtRenderer};
use all_is_cubes_ui::apps::Session;
use crate::glue::crossterm::{event_to_key, map_mouse_button};
use crate::session::DesktopSession;
mod chars;
mod options;
pub(crate) use options::TerminalOptions;
mod ray_image;
use ray_image::TextRayImage;
mod ui;
use ui::{write_ui, TerminalWindow};
pub(crate) fn terminal_print_once(
mut dsession: DesktopSession<TerminalRenderer, TerminalWindow>,
display_size: Vector2<u16>,
) -> Result<(), anyhow::Error> {
dsession.window.viewport_position = Rect::new(0, 0, display_size.x, display_size.y);
sync_viewport(&mut dsession);
dsession
.renderer
.send_frame_to_render(&mut dsession.session);
let frame = dsession
.renderer
.render_pipe_out
.recv()
.expect("Internal error in rendering");
dsession.window.write_frame(frame, false)?;
dsession.window.clean_up_terminal()?; Ok(())
}
pub(crate) struct TerminalRenderer {
cameras: StandardCameras,
options: TerminalOptions,
buffer_reuse_out: mpsc::Receiver<RtRenderer<CharacterRtData>>,
render_pipe_in: mpsc::SyncSender<FrameInput>,
render_pipe_out: mpsc::Receiver<TextRayImage>,
}
struct FrameInput {
options: TerminalOptions,
scene: RtRenderer<CharacterRtData>,
}
pub(crate) fn create_terminal_session(
session: Session,
options: TerminalOptions,
viewport_cell: ListenableCell<Viewport>,
) -> crossterm::Result<DesktopSession<TerminalRenderer, TerminalWindow>> {
viewport_cell.set(options.viewport_from_terminal_size(rect_size(Rect::default())));
let cameras = session.create_cameras(viewport_cell.as_source());
let number_of_scene_buffers = 3;
let (buffer_reuse_in, buffer_reuse_out) =
mpsc::sync_channel::<RtRenderer<CharacterRtData>>(number_of_scene_buffers);
for _ in 0..number_of_scene_buffers {
buffer_reuse_in
.send(RtRenderer::new(
cameras.clone(),
Box::new(|v| v),
ListenableSource::constant(()),
))
.unwrap();
}
let (render_pipe_in, render_thread_in) = mpsc::sync_channel::<FrameInput>(1);
let (render_thread_out, render_pipe_out) = mpsc::sync_channel(1);
std::thread::Builder::new()
.name("raytracer".to_string())
.spawn({
move || {
while let Ok(FrameInput { options, scene }) = render_thread_in.recv() {
let camera = &scene.cameras().cameras().world;
let viewport = scene.modified_viewport();
let mut image = vec![(String::new(), None); viewport.pixel_count().unwrap()];
let info = scene.draw::<ColorCharacterBuf, _, _, _>(
|_| String::new(),
|b| b.output(camera),
&mut image,
);
let _ = render_thread_out.send(TextRayImage {
viewport,
options,
image,
info,
});
let _ = buffer_reuse_in.send(scene);
}
}
})?;
Ok(DesktopSession::new(
TerminalRenderer {
cameras,
options,
buffer_reuse_out,
render_pipe_in,
render_pipe_out,
},
TerminalWindow::new()?,
session,
viewport_cell,
))
}
pub(crate) fn terminal_main_loop(
mut dsession: DesktopSession<TerminalRenderer, TerminalWindow>,
) -> Result<(), anyhow::Error> {
run(&mut dsession)?;
dsession.window.clean_up_terminal()?; Ok(())
}
fn run(dsession: &mut DesktopSession<TerminalRenderer, TerminalWindow>) -> crossterm::Result<()> {
dsession.window.begin_fullscreen()?;
loop {
'input: while crossterm::event::poll(Duration::ZERO)? {
let event = crossterm::event::read()?;
if let Some(aic_event) = event_to_key(&event) {
if dsession.session.input_processor.key_momentary(aic_event) {
continue 'input;
}
}
let options = &mut dsession.renderer.options;
match event {
Event::Key(
KeyEvent {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c' | 'd'),
modifiers: KeyModifiers::CONTROL,
..
},
) => {
return Ok(());
}
Event::Key(KeyEvent {
code: KeyCode::Char('n'),
modifiers: _,
..
}) => options.colors = options.colors.cycle(),
Event::Key(KeyEvent {
code: KeyCode::Char('m'),
modifiers: _,
..
}) => {
options.characters = options.characters.cycle();
sync_viewport(dsession);
}
Event::Key(_) => {}
Event::Resize(..) => { }
Event::Mouse(MouseEvent {
kind,
column,
row,
modifiers: _,
}) => {
let position =
Point2::new((f64::from(column) - 0.5) * 0.5, f64::from(row) - 0.5);
dsession.session.input_processor.mouse_pixel_position(
*dsession.viewport_cell.get(),
Some(position),
true,
);
match kind {
MouseEventKind::Down(button) => {
dsession.session.click(map_mouse_button(button));
}
MouseEventKind::Up(_)
| MouseEventKind::Drag(_)
| MouseEventKind::Moved
| MouseEventKind::ScrollDown
| MouseEventKind::ScrollUp => {}
}
}
Event::FocusGained | Event::FocusLost => {}
Event::Paste(_) => {}
}
}
dsession.advance_time_and_maybe_step();
match dsession.renderer.render_pipe_out.try_recv() {
Ok(frame) => {
write_ui(dsession, &frame)?;
dsession.window.write_frame(frame, true)?;
}
Err(mpsc::TryRecvError::Empty) => {}
Err(mpsc::TryRecvError::Disconnected) => panic!("render thread died"),
}
if dsession.session.frame_clock.should_draw() {
let c = &mut dsession.renderer.cameras;
c.update();
dsession.session.update_cursor(&*c);
dsession
.renderer
.send_frame_to_render(&mut dsession.session);
} else if let Some(t) = dsession.session.frame_clock.next_step_or_draw_time() {
std::thread::sleep(t - Instant::now());
}
}
}
impl TerminalRenderer {
fn send_frame_to_render(&mut self, session: &mut Session) {
let mut renderer = self.buffer_reuse_out.recv().unwrap();
renderer.update(session.cursor_result()).unwrap();
match self.render_pipe_in.try_send(FrameInput {
options: self.options.clone(),
scene: renderer,
}) {
Ok(()) => {
session.frame_clock.did_draw();
}
Err(TrySendError::Disconnected(_)) => {
}
Err(TrySendError::Full(_)) => {
}
}
}
}
fn sync_viewport(dsession: &mut DesktopSession<TerminalRenderer, TerminalWindow>) {
dsession.viewport_cell.set(
dsession
.renderer
.options
.viewport_from_terminal_size(rect_size(dsession.window.viewport_position)),
);
}
type TextAndColor = (String, Option<Rgba>);
#[derive(Clone, Debug, Default, PartialEq)]
struct ColorCharacterBuf {
color: ColorBuf,
text: CharacterBuf,
override_color: bool,
}
impl ColorCharacterBuf {
fn output(self, camera: &Camera) -> TextAndColor {
if self.override_color {
(self.text.into(), None)
} else {
(
self.text.into(),
Some(camera.post_process_color(Rgba::from(self.color))),
)
}
}
}
impl PixelBuf for ColorCharacterBuf {
type BlockData = <CharacterBuf as PixelBuf>::BlockData;
#[inline]
fn opaque(&self) -> bool {
self.color.opaque()
}
#[inline]
fn add(&mut self, surface_color: Rgba, text: &Self::BlockData) {
if self.override_color {
return;
}
self.color.add(surface_color, &());
self.text.add(surface_color, text);
}
fn hit_nothing(&mut self) {
self.text
.add(Rgba::TRANSPARENT, &CharacterRtData(Cow::Borrowed(" ")));
self.override_color = true;
}
fn mean<const N: usize>(mut items: [Self; N]) -> Self {
use std::{array::from_fn, mem::take};
Self {
color: ColorBuf::mean::<N>(from_fn(|i| items[i].color)),
text: CharacterBuf::mean::<N>(from_fn(|i| take(&mut items[i].text))),
override_color: items.into_iter().all(|ccb| ccb.override_color),
}
}
}
fn rect_size(rect: Rect) -> Vector2<u16> {
Vector2::new(rect.width, rect.height)
}