use std::io;
use bevy::{
ecs::system::{RunSystemOnce, SystemState},
prelude::*,
render::{
camera::RenderTarget, render_graph::RenderGraph, renderer::RenderDevice, Render, RenderApp,
RenderSet,
},
utils::{error, hashbrown::HashMap},
};
use bevy_ratatui::{event::ResizeEvent, terminal::RatatuiContext};
use crate::{
headless::{
image_copier_extract_system, receive_rendered_images_system, send_rendered_image_system,
HeadlessRenderPipe, ImageCopier, ImageCopy, ImageCopyNode,
},
RatatuiRenderWidget,
};
pub type AutoresizeConversionFn = fn((u32, u32)) -> (u32, u32);
pub struct RatatuiRenderPlugin {
id: String,
dimensions: (u32, u32),
print_full_terminal: bool,
autoresize: bool,
autoresize_conversion_fn: Option<AutoresizeConversionFn>,
disabled: bool,
}
impl RatatuiRenderPlugin {
pub fn new(id: &str, dimensions: (u32, u32)) -> Self {
Self {
id: id.into(),
dimensions,
print_full_terminal: false,
autoresize: false,
autoresize_conversion_fn: None,
disabled: false,
}
}
pub fn disable(mut self) -> Self {
self.disabled = true;
self
}
pub fn print_full_terminal(mut self) -> Self {
self.print_full_terminal = true;
self
}
pub fn autoresize(mut self) -> Self {
self.autoresize = true;
self
}
pub fn autoresize_conversion_fn(
mut self,
autoresize_conversion_fn: AutoresizeConversionFn,
) -> Self {
self.autoresize_conversion_fn = Some(autoresize_conversion_fn);
self
}
}
impl Plugin for RatatuiRenderPlugin {
fn build(&self, app: &mut App) {
if self.disabled {
app.init_resource::<RatatuiRenderContext>();
return;
}
if app
.world_mut()
.get_resource_mut::<RatatuiRenderContext>()
.is_none()
{
app.init_resource::<RatatuiRenderContext>()
.add_systems(First, receive_rendered_images_system)
.add_systems(PostUpdate, replaced_pipe_cleanup_system)
.add_event::<ReplacedRenderPipeEvent>();
let render_app = app.sub_app_mut(RenderApp);
let mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
graph.add_node(ImageCopy, ImageCopyNode);
graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy);
render_app
.add_systems(ExtractSchedule, image_copier_extract_system)
.add_systems(Render, send_rendered_image_system.after(RenderSet::Render));
}
app.add_systems(
PreStartup,
initialize_context_system_generator(self.id.clone(), self.dimensions),
);
if self.print_full_terminal {
app.add_systems(
Update,
print_full_terminal_system(self.id.clone()).map(error),
);
}
if self.autoresize {
app.add_systems(
PostStartup,
(
initial_resize_system,
autoresize_system_generator(self.id.clone(), self.autoresize_conversion_fn),
)
.chain(),
)
.add_systems(
PostUpdate,
autoresize_system_generator(self.id.clone(), self.autoresize_conversion_fn),
);
}
}
fn is_unique(&self) -> bool {
false
}
}
#[derive(Resource, Default, Deref, DerefMut)]
pub struct RatatuiRenderContext(HashMap<String, HeadlessRenderPipe>);
impl RatatuiRenderContext {
pub fn create(id: &str, dimensions: (u32, u32), world: &mut World) {
world.run_system_once(initialize_context_system_generator(id.into(), dimensions));
}
pub fn target(&self, id: &str) -> Option<RenderTarget> {
let pipe = self.get(id)?;
Some(pipe.target.clone())
}
pub fn dimensions(&self, id: &str) -> Option<(u32, u32)> {
let pipe = self.get(id)?;
Some((pipe.image.width(), pipe.image.height()))
}
pub fn widget(&self, id: &str) -> Option<RatatuiRenderWidget> {
let pipe = self.get(id)?;
Some(RatatuiRenderWidget::new(&pipe.image))
}
}
fn initialize_context_system_generator(
id: String,
dimensions: (u32, u32),
) -> impl FnMut(
Commands,
ResMut<Assets<Image>>,
Res<RenderDevice>,
ResMut<RatatuiRenderContext>,
EventWriter<ReplacedRenderPipeEvent>,
) {
move |mut commands, mut images, render_device, mut context, mut replaced_pipe| {
let new_pipe =
HeadlessRenderPipe::new(&mut commands, &mut images, &render_device, dimensions);
let new_pipe_target = new_pipe.target.clone();
let maybe_old_pipe = context.insert(id.clone(), new_pipe);
if let Some(old_pipe) = maybe_old_pipe {
replaced_pipe.send(ReplacedRenderPipeEvent {
old_render_target: old_pipe.target,
new_render_target: new_pipe_target,
});
}
}
}
fn print_full_terminal_system(
id: String,
) -> impl FnMut(ResMut<RatatuiContext>, Res<RatatuiRenderContext>) -> io::Result<()> {
move |mut ratatui, ratatui_render| {
if let Some(render_widget) = ratatui_render.widget(&id) {
ratatui.draw(|frame| {
frame.render_widget(render_widget, frame.area());
})?;
}
Ok(())
}
}
fn initial_resize_system(
ratatui: Res<RatatuiContext>,
mut resize_events: EventWriter<ResizeEvent>,
) {
if let Ok(size) = ratatui.size() {
resize_events.send(ResizeEvent(size));
}
}
fn autoresize_system_generator(
id: String,
conversion_fn: Option<AutoresizeConversionFn>,
) -> impl FnMut(&mut World) {
move |world| {
let mut system_state: SystemState<EventReader<ResizeEvent>> = SystemState::new(world);
let mut ratatui_events = system_state.get_mut(world);
if let Some(ResizeEvent(dimensions)) = ratatui_events.read().last() {
let terminal_dimensions = (dimensions.width as u32, dimensions.height as u32 * 2);
let conversion_fn = conversion_fn.unwrap_or(|(width, height)| (width * 2, height * 2));
let new_dimensions = conversion_fn(terminal_dimensions);
RatatuiRenderContext::create(&id, new_dimensions, world);
}
}
}
#[derive(Event)]
pub struct ReplacedRenderPipeEvent {
old_render_target: RenderTarget,
new_render_target: RenderTarget,
}
fn replaced_pipe_cleanup_system(
mut commands: Commands,
mut replaced_pipe: EventReader<ReplacedRenderPipeEvent>,
mut images: ResMut<Assets<Image>>,
mut camera_query: Query<&mut Camera>,
mut image_copier_query: Query<(Entity, &mut ImageCopier)>,
) {
for ReplacedRenderPipeEvent {
old_render_target,
new_render_target,
} in replaced_pipe.read()
{
if let Some(old_target_image) = old_render_target.as_image() {
if let Some(mut camera) = camera_query.iter_mut().find(|camera| {
if let Some(camera_image) = camera.target.as_image() {
return camera_image == old_target_image;
}
false
}) {
camera.target = new_render_target.clone();
if let Some(image_handle) = old_render_target.as_image() {
images.remove(image_handle);
}
}
if let Some((entity, image_copier)) = image_copier_query
.iter_mut()
.find(|(_, image_copier)| image_copier.src_image == *old_target_image)
{
commands.entity(entity).despawn();
images.remove(&image_copier.src_image.clone());
}
};
}
}