ftui-extras 0.3.1

Feature-gated extras for FrankenTUI (markdown, charts, clipboard, themes).
Documentation
#![forbid(unsafe_code)]

//! Quake Console effect.
//!
//! A drop-down console background with a procedural grunge/cloud texture.
//! Simulates the look of Quake's `conback.lmp` using fractal noise.

#[cfg(feature = "canvas")]
use crate::canvas::Painter;
use crate::visual_fx::{BackdropFx, FxContext};
use ftui_render::cell::PackedRgba;

/// Quake-style drop-down console effect.
pub struct QuakeConsoleFx {
    /// Current drop extension (0.0 to 1.0).
    drop_progress: f32,
    /// Texture buffer to avoid recomputing noise every frame.
    texture: Vec<PackedRgba>,
    texture_size: (u16, u16),
}

impl QuakeConsoleFx {
    pub fn new() -> Self {
        Self {
            drop_progress: 1.0,
            texture: Vec::new(),
            texture_size: (0, 0),
        }
    }

    pub fn set_progress(&mut self, progress: f32) {
        self.drop_progress = progress.clamp(0.0, 1.0);
    }

    /// Generate a procedural grunge texture.
    fn generate_texture(
        &mut self,
        width: u16,
        height: u16,
        base_color: PackedRgba,
        highlight_color: PackedRgba,
    ) {
        let w = width as usize;
        let h = height as usize;
        let len = w * h;

        self.texture.resize(len, PackedRgba::default());
        self.texture_size = (width, height);

        // Simple value noise / fractional brownian motion approximation
        let mut rng = 0xCAFEBABE_u32;
        let mut rand = || {
            rng = rng.wrapping_mul(1664525).wrapping_add(1013904223);
            (rng >> 16) as f32 / 65536.0
        };

        // Generate noise
        for y in 0..h {
            for x in 0..w {
                // Coordinate scaling for "cloudy" look
                let nx = x as f32 * 0.1;
                let ny = y as f32 * 0.2;

                // Cheap noise: sin combination
                let v = (nx.sin() + ny.cos() + rand() * 0.5).abs().clamp(0.0, 1.0);

                // Mix colors
                let pixel = highlight_color.with_opacity(v * 0.3).over(base_color);

                self.texture[y * w + x] = pixel;
            }
        }
    }

    /// Render to a sub-pixel painter (high-resolution console).
    #[cfg(feature = "canvas")]
    pub fn render_painter(&mut self, painter: &mut Painter, theme: &crate::visual_fx::ThemeInputs) {
        let (w, h) = painter.size();
        let width = w as usize;
        let height = h as usize;
        let drop_height = (height as f32 * self.drop_progress).round() as usize;

        if self.texture_size != (w, h) {
            self.generate_texture(w, h, theme.bg_base, theme.bg_surface);
        }

        for y in 0..drop_height {
            let row_start = y * width;
            // Blit texture pixel by pixel
            for x in 0..width {
                let color = self.texture[row_start + x];
                painter.point_colored(x as i32, y as i32, color);
            }
        }

        // Draw the bottom edge/highlight
        if drop_height < height && drop_height > 0 {
            let y = (drop_height - 1) as i32;
            let color = theme.accent_primary;
            painter.line_colored(0, y, (width - 1) as i32, y, Some(color));
        }
    }
}

impl BackdropFx for QuakeConsoleFx {
    fn name(&self) -> &'static str {
        "quake-console"
    }

    fn resize(&mut self, _width: u16, _height: u16) {}

    fn render(&mut self, ctx: FxContext<'_>, out: &mut [PackedRgba]) {
        if ctx.is_empty() {
            return;
        }

        let width = ctx.width as usize;
        let height = ctx.height as usize;
        let drop_height = (height as f32 * self.drop_progress).round() as usize;

        if self.texture_size != (ctx.width, ctx.height) {
            self.generate_texture(
                ctx.width,
                ctx.height,
                ctx.theme.bg_base,
                ctx.theme.bg_surface,
            );
        }

        // Blit texture for the dropped portion
        for y in 0..drop_height {
            let row_start = y * width;
            let src_row = &self.texture[row_start..row_start + width];
            let dst_row = &mut out[row_start..row_start + width];
            dst_row.copy_from_slice(src_row);
        }

        // Draw the bottom edge/highlight
        if drop_height < height && drop_height > 0 {
            let y = drop_height - 1;
            for x in 0..width {
                let idx = y * width + x;
                out[idx] = ctx.theme.accent_primary;
            }
        }
    }
}

impl Default for QuakeConsoleFx {
    fn default() -> Self {
        Self::new()
    }
}