germterm 0.4.0

A lightweight high-performance terminal graphics framework!
Documentation
//! Core engine orchestration and frame management.
//!
//! This module ties together the terminal, frame, drawing layers, FPS, and particle state.
//! It provides the primary functions needed to initialize the terminal, start and end the frame, and render output.
//! Essentially, this is the central "body" that coordinates everything.

use crate::{
    color::{Color, ColorRgb},
    draw::erase_rect,
    fps_counter::{FpsCounter, update_fps_counter},
    fps_limiter::{self, FpsLimiter, wait_for_next_frame},
    frame::{FramePair, compose_frame_buffer, draw_to_terminal},
    layer::{Layer, LayerIndex, create_layer},
    particle::{ParticleState, update_and_draw_particles},
};
use crossterm::{cursor, event, execute, terminal};
use std::{
    io::{self},
    time::Duration,
};

pub struct Engine {
    pub delta_time: f32,
    pub game_time: f32,
    pub stdout: io::Stdout,
    pub(crate) default_blending_color: Color,
    pub(crate) fps_counter: FpsCounter,
    pub(crate) max_layer_index: usize,
    pub(crate) frame: FramePair,
    pub(crate) fps_limiter: FpsLimiter,
    pub(crate) particle_state: Vec<ParticleState>,
    title: &'static str,
}

impl Engine {
    pub fn new(cols: u16, rows: u16) -> Self {
        Self {
            delta_time: 0.01667,
            game_time: 0.0,
            title: "my-awesome-terminal",
            stdout: io::stdout(),
            max_layer_index: 0,
            frame: FramePair::new(cols, rows),
            fps_limiter: FpsLimiter::new(60, 0.001, 0.002),
            fps_counter: FpsCounter::new(0.3),
            particle_state: Vec::with_capacity(512),
            default_blending_color: {
                match termbg::rgb(Duration::from_millis(100)) {
                    Ok(rgb) => Color::new(rgb.r as u8, rgb.g as u8, rgb.b as u8, 255),
                    Err(_) => Color::BLACK,
                }
            },
        }
    }

    pub fn title(mut self, value: &'static str) -> Self {
        self.title = value;
        self
    }

    /// A value of `0` will result in uncapped FPS.
    pub fn limit_fps(mut self, value: u32) -> Self {
        fps_limiter::limit_fps(&mut self.fps_limiter, value);
        self
    }
}

/// Overrides the default blending color.
///
/// Only use this if you need to support terminals where the background color cannot
/// be reliably auto-detected by `termbg`. Otherwise, it's best to leave this alone.
pub fn override_default_blending_color(engine: &mut Engine, color: ColorRgb) {
    engine.default_blending_color = color.into();
}

/// This function should be called once after constructing the [`Engine`] and defining layers,
/// and before entering the main update loop to initialize the engine.
///
/// # Example
/// ```rust,no_run
/// # use germterm::{layer::create_layer, engine::{Engine, init}};
/// let mut engine = Engine::new(40, 20);
/// let layer = create_layer(&mut engine, 0);
/// init(&mut engine);
/// ```
pub fn init(engine: &mut Engine) -> io::Result<()> {
    let layer_count = engine.max_layer_index + 1;
    if engine.frame.layered_draw_queue.len() < layer_count {
        engine
            .frame
            .layered_draw_queue
            .resize_with(layer_count, Layer::new);
    }

    terminal::enable_raw_mode()?;
    execute!(
        engine.stdout,
        terminal::EnterAlternateScreen,
        terminal::SetTitle(engine.title),
        event::EnableMouseCapture,
        cursor::Hide,
    )?;
    Ok(())
}

/// Cleans up the terminal state and exits the altenate screen.
///
/// Not calling ['exit_cleanup'] before exiting the program
/// will result in a messed up terminal state. (Be nice, clean up after yourself!)
pub fn exit_cleanup(engine: &mut Engine) -> io::Result<()> {
    terminal::disable_raw_mode()?;
    execute!(
        engine.stdout,
        terminal::LeaveAlternateScreen,
        terminal::EnableLineWrap,
        cursor::Show,
        event::DisableMouseCapture
    )?;
    Ok(())
}

/// Prepares a fresh frame state.
///
/// This function should be called once at the start of each frame inside the update loop.
///
/// Drawing should only happen after this is called for predictable results.
pub fn start_frame(engine: &mut Engine) {
    engine.delta_time = wait_for_next_frame(&mut engine.fps_limiter);
    update_fps_counter(&mut engine.fps_counter, engine.delta_time);

    let lowest_layer_index: LayerIndex = create_layer(engine, 0);
    erase_rect(
        engine,
        lowest_layer_index,
        0,
        0,
        engine.frame.width as i16,
        engine.frame.height as i16,
    );
}

/// Renders the contents to the terminal and ends the frame.
///
/// This function should be called once at the end of each frame inside the update loop.
///
/// No drawing should be happening after this function is called in the update loop.
pub fn end_frame(engine: &mut Engine) -> io::Result<()> {
    update_and_draw_particles(engine);

    let height = engine.frame.height;
    let width = engine.frame.width;
    let (current, layered) = engine.frame.current_mut_and_layered_mut();
    compose_frame_buffer(
        current,
        layered.iter_mut().flat_map(|v| v.0.drain(..)),
        width,
        height,
        engine.default_blending_color,
    );
    let diff_products = engine.frame.diff();
    draw_to_terminal(&mut engine.stdout, diff_products)?;
    engine.frame.swap_frames();

    engine.game_time += engine.delta_time;
    Ok(())
}