bevy_ratatui_camera 0.16.0

A bevy plugin for rendering your bevy app to the terminal using ratatui.
Documentation
use std::fmt::Debug;

use bevy::prelude::{Component, Entity};
use image::DynamicImage;
use ratatui::widgets::{StatefulWidgetRef, Widget};
use ratatui::{prelude::*, widgets::WidgetRef};

use crate::widget_depth_buffer::RatatuiCameraDepthBuffer;
use crate::widget_strategy_depth::RatatuiCameraWidgetDepth;
use crate::widget_strategy_halfblocks::RatatuiCameraWidgetHalf;
use crate::widget_strategy_luminance::RatatuiCameraWidgetLuminance;
use crate::widget_strategy_none::RatatuiCameraWidgetNone;
use crate::{RatatuiCameraEdgeDetection, RatatuiCameraStrategy};

/// Ratatui widget that will be inserted into each RatatuiCamera containing entity and updated each
/// frame with the last image rendered by the camera. When drawn in a ratatui buffer, it will use
/// the RatatuiCamera's specified RatatuiCameraStrategy to convert the rendered image to unicode
/// characters, and will draw them in the buffer.
///
#[derive(Component, Debug)]
pub struct RatatuiCameraWidget {
    /// Associated entity.
    pub entity: Entity,

    /// RatatuiCamera camera's rendered image copied back from the GPU.
    pub camera_image: DynamicImage,

    /// RatatuiCamera camera's depth texture copied back from the GPU.
    pub depth_image: Option<DynamicImage>,

    /// RatatuiCamera camera's sobel texture generated by the GPU, if any.
    pub sobel_image: Option<DynamicImage>,

    /// Strategy used to convert the rendered image to unicode.
    pub strategy: RatatuiCameraStrategy,

    /// RatatuiCamera's edge detection settings, if any.
    pub edge_detection: Option<RatatuiCameraEdgeDetection>,

    /// The area this widget was rendered within last frame.
    pub last_area: Rect,

    /// The area this widget was most recently rendered within, which will replace `last_area`
    /// before the camera widget is available to render next frame.
    pub(crate) next_last_area: Rect,
}

impl Widget for &mut RatatuiCameraWidget {
    fn render(self, area: Rect, buf: &mut Buffer) {
        self.render_common(area, buf, None);
    }
}

impl StatefulWidget for &mut RatatuiCameraWidget {
    type State = RatatuiCameraDepthBuffer;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        self.render_common(area, buf, Some(state));
    }
}

impl RatatuiCameraWidget {
    /// Check for a change in area since last frame, updating the `next_last_area` attribute to
    /// trigger a resize if necessary. Returns `true` if the area changed, otherwise `false`.
    fn area_check(&mut self, area: Rect) -> bool {
        if self.last_area != area {
            self.next_last_area = area;
            return true;
        }

        false
    }

    /// Common render method shared by the Widget and StatefulWidget `render()` implementations.
    fn render_common(
        &mut self,
        area: Rect,
        buf: &mut Buffer,
        depth_buffer: Option<&mut RatatuiCameraDepthBuffer>,
    ) {
        if self.area_check(area) {
            return;
        }

        let render_area = self.calculate_render_area(area);
        let (camera_image, depth_image, sobel_image) = self.resize_images_to_area(render_area);

        match self.strategy {
            RatatuiCameraStrategy::HalfBlocks(ref strategy_config) => {
                RatatuiCameraWidgetHalf::new(
                    camera_image,
                    depth_image,
                    sobel_image,
                    depth_buffer,
                    strategy_config,
                    &self.edge_detection,
                )
                .render(render_area, buf);
            }
            RatatuiCameraStrategy::Depth(ref strategy_config) => {
                RatatuiCameraWidgetDepth::new(
                    camera_image,
                    depth_image,
                    sobel_image,
                    depth_buffer,
                    strategy_config,
                    &self.edge_detection,
                )
                .render(render_area, buf);
            }
            RatatuiCameraStrategy::Luminance(ref strategy_config) => {
                RatatuiCameraWidgetLuminance::new(
                    camera_image,
                    depth_image,
                    sobel_image,
                    depth_buffer,
                    strategy_config,
                    &self.edge_detection,
                )
                .render(render_area, buf);
            }
            RatatuiCameraStrategy::None => {
                RatatuiCameraWidgetNone::new(camera_image, sobel_image, &self.edge_detection)
                    .render_ref(render_area, buf);
            }
        }
    }

    /// Create a depth buffer that can be used for occlusion effects. Pass the resulting buffer
    /// into this widget's `StatefulWidget::render()` implementation to record depths from the
    /// associated camera's depth prepass (if present). Pass the same buffer into other camera
    /// render methods and into `render_overlay_with_depth()` in order to record their depths as
    /// well, skipping terminal cells when they would be occluded.
    ///
    /// Note that objects will only occlude if they show up in Bevy's render prepass, so please
    /// consult Bevy's documentation on what is excluded.
    pub fn new_depth_buffer(&self, area: Rect) -> RatatuiCameraDepthBuffer {
        let render_area = self.calculate_render_area(area);
        RatatuiCameraDepthBuffer::new(render_area)
    }

    /// Draw an "overlay" widget using the same calculated render area as the camera widget.
    ///
    /// Using this method rather than directly calling `render()` on the widget provides two
    /// benefits:
    ///
    /// - The widget will be rendered using the same calculated render area used for drawing the
    ///   camera render (e.g. when empty gutters are used to preserve aspect ratio, overlay widgets
    ///   will have their render methods called with an area excluding those gutters automatically).
    ///
    /// - Rendering of the overlay widget will be skipped when the camera image render is skipped
    ///   (when the draw area has changed since the last frame and the render texture needs to be
    ///   resized), which prevents the widgets from "flashing" in the wrong place for one frame.
    ///
    /// If you need more control over rendering the widgets but would still like these two
    /// behaviors:
    ///
    /// - Call `calculate_render_area()` on your `RatatuiCameraWidget` to get the correct
    ///   area that the camera render will actually display (not necessary if autoresize is turned
    ///   on, as aspect ratio is not preserved and the result will always match the input area).
    ///
    /// - Compare the `last_area` attribute on your `RatatuiCameraWidget` to this frame's area, and
    ///   skip rendering the overlay widgets for this frame if they differ.
    pub fn render_overlay(&self, area: Rect, buf: &mut Buffer, widget: &dyn WidgetRef) {
        if self.last_area != area {
            return;
        }

        let render_area = self.calculate_render_area(area);

        widget.render_ref(render_area, buf);
    }

    /// See [RatatuiCameraWidget::render_overlay]. This variant additionally passes in a depth
    /// buffer as state to the ratatui widget, allowing a ratatui widget to achieve occlusion
    /// effects by:
    ///
    /// - Associating bevy world-space depths with the widget cells to be drawn.
    ///
    /// - Comparing against and updating the depth buffer using those depths.
    ///
    /// - Only drawing the new cell when it is "closer" to the camera than whatever is previously
    ///   recorded in the depth buffer.
    pub fn render_overlay_with_depth(
        &mut self,
        area: Rect,
        buf: &mut Buffer,
        widget: &dyn StatefulWidgetRef<State = RatatuiCameraDepthBuffer>,
        depth_buffer: &mut RatatuiCameraDepthBuffer,
    ) {
        if self.last_area != area {
            return;
        }

        let render_area = self.calculate_render_area(area);

        widget.render_ref(render_area, buf, depth_buffer);
    }
}