ratatui-wgpu 0.1.2

A wgpu based backend for ratatui.
Documentation
use std::ops::Deref;

use ratatui::style::Modifier;

use crate::{
    utils::lru::Lru,
    Fonts,
};

#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub(crate) struct Key {
    pub(crate) style: Modifier,
    pub(crate) glyph: u32,
    pub(crate) font: u64,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) struct CacheRect {
    pub(crate) x: u32,
    pub(crate) y: u32,
    pub(crate) width: u32,
    pub(crate) height: u32,
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum Entry {
    Cached(CacheRect),
    Uncached(CacheRect),
}

impl Entry {
    pub(crate) fn cached(&self) -> bool {
        matches!(self, Entry::Cached(_))
    }
}

impl Deref for Entry {
    type Target = CacheRect;

    fn deref(&self) -> &Self::Target {
        let (Entry::Cached(entry) | Entry::Uncached(entry)) = self;
        entry
    }
}

#[derive(Debug)]
pub(crate) struct Atlas {
    lru: Lru<Key, CacheRect>,
    width: u32,
    height: u32,

    entry_width: u32,
    entry_height: u32,

    next_entry: u32,
    max_entries: u32,
}

impl Atlas {
    pub(crate) fn new(fonts: &Fonts, width: u32, height: u32) -> Self {
        let entry_width = fonts.width_px() * 2;
        let entry_height = fonts.height_px();
        let max_entries = (width / entry_width) * (height / entry_height);
        debug!("Atlas with WxH {entry_width}x{entry_height} can hold {max_entries}");

        Atlas {
            lru: Lru::default(),
            width,
            height,
            entry_width,
            entry_height,
            next_entry: 0,
            max_entries,
        }
    }

    pub(crate) fn match_fonts(&mut self, fonts: &Fonts) {
        self.clear();
        self.entry_width = fonts.width_px() * 2;
        self.entry_height = fonts.height_px();
        self.max_entries = (self.width / self.entry_width) * (self.height / self.entry_height);

        debug!(
            "Atlas with WxH {}x{} can hold {}",
            self.entry_width, self.entry_height, self.max_entries
        );
    }

    fn clear(&mut self) {
        self.lru.clear();
        self.next_entry = 0;
    }

    pub(crate) fn try_get(&mut self, key: &Key) -> Option<Entry> {
        self.lru.get(key).copied().map(Entry::Cached)
    }

    pub(crate) fn get(&mut self, key: &Key, width: u32, height: u32) -> Entry {
        debug_assert_eq!(
            self.entry_height, height,
            "Internal height not equal to provided height. Did you forget to call match_fonts?"
        );
        debug_assert_eq!(
            self.entry_width % width,
            0,
            "Internal width not a multiple of provided width. Did you forget to call match_fonts?"
        );

        self.try_get(key).unwrap_or_else(|| {
            let rect = if self.next_entry == self.max_entries {
                self.lru.pop().expect("Atlas has zero max entries!").1
            } else {
                self.slot_to_rect(self.next_entry, width)
            };

            self.next_entry += 1;
            self.lru.insert(*key, rect);
            Entry::Uncached(rect)
        })
    }

    fn slot_to_rect(&self, slot: u32, width: u32) -> CacheRect {
        let x = slot % (self.width / self.entry_width) * self.entry_width;
        let y = slot / (self.width / self.entry_width) * self.entry_height;
        CacheRect {
            x,
            y,
            width,
            height: self.entry_height,
        }
    }
}

#[cfg(test)]
mod tests {
    use ratatui::style::Modifier;

    use crate::{
        utils::text_atlas::{
            Atlas,
            Key,
        },
        Font,
        Fonts,
    };

    #[test]
    fn reuse() {
        let fonts = Fonts::new(
            Font::new(include_bytes!(concat!(
                env!("CARGO_MANIFEST_DIR"),
                "/src/backend/fonts/Fairfax.ttf"
            )))
            .unwrap(),
            24,
        );
        let mut atlas = Atlas::new(&fonts, 24, 24);

        for idx in 0..atlas.max_entries {
            atlas.get(
                &Key {
                    style: Modifier::default(),
                    glyph: idx as _,
                    font: idx as _,
                },
                12,
                24,
            );
        }

        let last_key = Key {
            style: Modifier::default(),
            glyph: u32::MAX,
            font: u32::MAX as _,
        };

        let last_inserted = atlas.get(&last_key, 12, 24);
        let post_insertion = atlas.get(&last_key, 12, 24);

        assert_eq!(*last_inserted, *post_insertion);
    }
}