ltrender 0.1.3

a diff based terminal rendering engine
Documentation
use crate::{
    DrawError, DrawObject, ObjectId, SpriteRegistry, UpdateInterval,
    terminal_buffer::{
        CellDrawer, CharacterInfo, CharacterInfoList, ScreenBufferCore, drawable::BasicDraw,
    },
    update_interval_handler::UpdateIntervalCreator,
};
use common_stdx::Rect;
use std::{collections::HashMap, fmt::Debug};

use ascii_assets::Color;
use ascii_assets::TerminalChar;
use common_stdx::Point;

#[derive(Debug, Clone)]
pub enum CellDrawerCommand {
    SetString(BatchDrawInfo, (u16, u16)),
    Flush,
    Stop,
}

#[derive(Debug, Clone)]
pub struct BatchSegment {
    pub text: String,
    pub fg_color: Option<Color>,
    pub bg_color: Option<Color>,
}

#[derive(Debug, Clone)]
pub struct BatchDrawInfo {
    pub start_x: u16,
    pub y: u16,
    pub segments: Vec<BatchSegment>,
}

pub trait ScreenBuffer: ScreenBufferCore {
    type Drawer: CellDrawer + Send + 'static;
    fn new(size: (u16, u16)) -> Self
    where
        Self: Sized;

    /// Add an object to the buffer.
    fn add_to_buffer(
        &mut self,
        obj: &mut DrawObject,
        obj_id: ObjectId,
        screen_layer: usize,
        bounds: &Rect<i32>,
        sprites: &SpriteRegistry,
    ) -> Result<(), DrawError> {
        let drawable = &mut obj.drawable;

        let mut draws = drawable.draw(sprites)?.dump_draws();

        // TODO: make this only necessary, if a shader is applied, that requires the top left corner
        let top_left = if let Some(corner) = drawable.get_top_left() {
            corner
        } else {
            Point { x: 0, y: 0 }
        };
        let opt_c = drawable.bounding_iv(sprites);
        // TODO: is this right??
        _ = self.handle_none_interval_creator(opt_c, bounds.p1);
        let size = drawable.size(sprites)?;
        for unshifted_bd in draws.iter_mut() {
            // using the top left corner of the screen, to shift the drawables position on screen
            let mut rd = BasicDraw {
                pos: unshifted_bd.pos + bounds.p1,
                chr: unshifted_bd.chr,
            };

            for shader in &obj.shaders {
                shader.apply(
                    &mut rd,
                    (size.0 as usize, size.1 as usize),
                    top_left + bounds.p1,
                );
            }
            if !bounds.contains(rd.pos) {
                continue;
            }

            if rd.pos.x < 0 || rd.pos.y < 0 {
                continue;
            }
            let (buf_cols, buf_rows) = self.size();
            if rd.pos.x as u16 >= buf_cols || rd.pos.y as u16 >= buf_rows {
                continue;
            }

            let ci = CharacterInfo {
                chr: rd.chr,
                layer: obj.layer,
                screen_layer,
                display_id: obj_id,
            };
            let idx = self.idx_of(rd.pos);
            self.cell_info_mut()[idx].info.insert(obj_id, ci);
        }

        Ok(())
    }

    /// Remove an object from the buffer.
    fn remove_from_buffer(
        &mut self,
        obj: &DrawObject,
        obj_id: ObjectId,
        sprites: &SpriteRegistry,
        bounds: &Rect<i32>,
    ) {
        let drawable = &*obj.drawable;
        let opt_c = drawable.bounding_iv(sprites);

        let ivs: HashMap<u16, Vec<UpdateInterval>> =
            self.handle_none_interval_creator(opt_c, bounds.p1);
        let (buf_cols, buf_rows) = self.size();

        for (row, iv_list) in ivs.into_iter() {
            if row >= buf_rows {
                continue;
            }

            for iv in iv_list {
                let (start, end) = iv.interval;
                let clamped_end = end.min(buf_cols as usize);
                if start >= clamped_end {
                    continue;
                }

                for x in start..clamped_end {
                    let pos = Point {
                        x: x as i32,
                        y: row as i32,
                    };
                    let idx = self.idx_of(pos);
                    self.cell_info_mut()[idx].info.remove(&obj_id);
                }
            }
        }
    }

    fn update_terminal(&mut self, expand: usize) -> Result<(), DrawError> {
        self.intervals_mut().expand_regions(expand);
        self.intervals_mut().merge_intervals();
        let intervals = self.intervals_mut().dump_intervals();
        let (cols, rows) = self.size();

        for iv in intervals {
            let (start, end) = iv.interval;
            let max_idx = cols as usize * rows as usize;
            let start = start.min(max_idx);
            let end = end.min(max_idx);

            let cells = self.cell_info_mut();
            let mut batch = BatchDrawInfo {
                start_x: 0,
                y: 0,
                segments: Vec::new(),
            };
            let mut current_segment: Option<BatchSegment> = None;

            let slice = &cells[start..end];
            for (i, cell) in slice.iter().enumerate() {
                let idx = start + i;
                let y = (idx / cols as usize) as u16;
                let x = (idx % cols as usize) as u16;

                let chr_to_write = if let Some(ci_opt) =
                    cell.info.values().max_by_key(|c| (c.screen_layer, c.layer))
                {
                    ci_opt.chr
                } else {
                    TerminalChar {
                        chr: ' ',
                        fg_color: None,
                        bg_color: None,
                    }
                };

                let colors = (chr_to_write.fg_color, chr_to_write.bg_color);

                if current_segment.is_none() {
                    current_segment = Some(BatchSegment {
                        text: chr_to_write.chr.to_string(),
                        fg_color: colors.0,
                        bg_color: colors.1,
                    });
                    batch.start_x = x;
                    batch.y = y;
                } else if let Some(seg) = &mut current_segment {
                    if seg.fg_color == colors.0 && seg.bg_color == colors.1 {
                        seg.text.push(chr_to_write.chr);
                    } else {
                        batch.segments.push(seg.clone());
                        *seg = BatchSegment {
                            text: chr_to_write.chr.to_string(),
                            fg_color: colors.0,
                            bg_color: colors.1,
                        };
                    }
                }
            }

            if let Some(seg) = current_segment {
                batch.segments.push(seg);
            }

            if let Err(e) = self
                .drawer_sender()
                .send(CellDrawerCommand::SetString(batch, self.size()))
            {
                log::error!("Failed to send SetString to drawer thread: {}", e);
            }
        }

        if let Err(e) = self.drawer_sender().send(CellDrawerCommand::Flush) {
            log::error!("Failed to send Flush to drawer thread: {}", e);
        }
        Ok(())
    }

    fn mark_all_dirty(&mut self, new_size: (u16, u16)) {
        self.invalidate_entire_screen();
        let capacity = new_size.0 as usize * new_size.1 as usize;
        if self.cell_info_mut().len() != capacity {
            self.cell_info_mut()
                .resize_with(capacity, || CharacterInfoList {
                    info: HashMap::new(),
                });
        }
    }
    fn drawer_sender(&self) -> std::sync::mpsc::SyncSender<CellDrawerCommand>;

    fn idx_of(&self, pos: Point<i32>) -> usize {
        (pos.y as usize) * self.size().0 as usize + pos.x as usize
    }

    fn drop(&mut self);

    fn handle_none_interval_creator(
        &mut self,
        opt_c: Option<UpdateIntervalCreator>,
        shift_amount: Point<i32>,
    ) -> HashMap<u16, Vec<UpdateInterval>> {
        if let Some(mut c) = opt_c {
            c.shift_all_intervals_by(shift_amount);
            self.merge_creator(c.clone());
            c.dump_intervals()
        } else {
            let mut c = UpdateIntervalCreator::new();
            c.register_redraw_region(Rect {
                p1: Point { x: 0, y: 0 },
                p2: Point {
                    x: self.size().0 as i32,
                    y: self.size().1 as i32,
                },
            });
            self.merge_creator(c.clone());
            c.dump_intervals()
        }
    }
}