use std::{num::NonZeroU32, rc::Rc};
use cranpose_core::collections::pass_aged::PassAgedCache;
use cranpose_ui_graphics::MemoryHeadroom;
use smallvec::SmallVec;
use super::{
TextAlign,
measure::{PreparedTextLayout, TextFrame},
style::TextStyle,
text_key::TextKey,
};
const TEXT_PREPARED_CACHE_CAPACITY: usize = 1024;
const WIDTHS_PER_TEXT: usize = 2;
const DRAWN_LAYOUTS: usize = 8;
const RETURNING_SLOTS: usize = 128;
type Widths = SmallVec<[Rc<PreparedTextLayout>; WIDTHS_PER_TEXT]>;
pub(super) enum Cached {
Held(Rc<PreparedTextLayout>),
Missed(Miss),
}
#[derive(Clone, Copy)]
pub(super) enum Miss {
OtherWidths,
Absent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CacheAdmission {
First,
Changed,
Drawn,
}
pub(super) struct LayoutCache {
kept: PassAgedCache<TextKey, Widths>,
drawn: DrawnLayouts,
returning: ReturningKeys,
headroom: MemoryHeadroom,
}
impl LayoutCache {
pub(super) fn new() -> Self {
Self {
kept: PassAgedCache::with_capacity_at_least_one(TEXT_PREPARED_CACHE_CAPACITY),
drawn: DrawnLayouts::default(),
returning: ReturningKeys::default(),
headroom: MemoryHeadroom::default(),
}
}
pub(super) fn clear(&mut self) {
self.kept.clear();
self.drawn = DrawnLayouts::default();
self.returning = ReturningKeys::default();
}
pub(super) fn cached(
&mut self,
key: TextKey,
frame: TextFrame,
(style, admission): (&TextStyle, CacheAdmission),
) -> Cached {
let align = style.paragraph_style.text_align;
if admission == CacheAdmission::Drawn
&& let Some(drawn) = self.drawn.get(key, frame, align)
{
return Cached::Held(drawn);
}
let Some(widths) = self.kept.get_mut(&key) else {
return Cached::Missed(Miss::Absent);
};
let Some(held) = widths
.iter()
.find(|prepared| prepared.layout.holds(frame.min_width, frame.max_width))
.map(Rc::clone)
else {
return Cached::Missed(Miss::OtherWidths);
};
let aligned = held.at_widths((frame.min_width, frame.max_width), align, None);
if !Rc::ptr_eq(&aligned, &held) {
remember(widths, &aligned);
}
Cached::Held(aligned)
}
pub(super) fn keep(
&mut self,
key: TextKey,
prepared: &Rc<PreparedTextLayout>,
(miss, admission): (Miss, CacheAdmission),
) {
if let Miss::OtherWidths = miss {
if let Some(widths) = self.kept.get_mut(&key) {
remember(widths, prepared);
}
return;
}
match admission {
CacheAdmission::First => {}
_ if self.headroom == MemoryHeadroom::Tight => return,
CacheAdmission::Changed | CacheAdmission::Drawn => {
let hash = key.mixed_hash();
if !self.returning.contains(hash) {
self.returning.put(hash);
if admission == CacheAdmission::Drawn {
self.drawn.push(key, Rc::clone(prepared));
}
return;
}
}
}
self.keep_first(key, prepared);
}
#[inline(never)]
fn keep_first(&mut self, key: TextKey, prepared: &Rc<PreparedTextLayout>) {
self.kept
.push(key, SmallVec::from_elem(Rc::clone(prepared), 1));
}
pub(super) fn begin_pass(&mut self, idle_passes: u64, headroom: MemoryHeadroom) {
self.kept.begin_pass(idle_passes, drop);
self.drawn.begin_pass();
self.headroom = headroom;
}
}
#[inline(never)]
fn remember(widths: &mut Widths, prepared: &Rc<PreparedTextLayout>) {
widths.truncate(WIDTHS_PER_TEXT - 1);
widths.insert(0, Rc::clone(prepared));
}
#[derive(Default)]
struct DrawnLayouts {
slots: Vec<DrawnLayout>,
pass: u64,
}
struct DrawnLayout {
key: TextKey,
prepared: Rc<PreparedTextLayout>,
pass: u64,
}
impl DrawnLayouts {
fn get(
&mut self,
key: TextKey,
frame: TextFrame,
align: TextAlign,
) -> Option<Rc<PreparedTextLayout>> {
let index = self.slots.iter().position(|slot| slot.key == key)?;
let pass = self.pass;
let slot = self.slots.get_mut(index)?;
if !slot.prepared.layout.holds(frame.min_width, frame.max_width) {
self.slots.swap_remove(index);
return None;
}
slot.prepared = slot
.prepared
.at_widths((frame.min_width, frame.max_width), align, None);
slot.pass = pass;
Some(Rc::clone(&slot.prepared))
}
fn push(&mut self, key: TextKey, prepared: Rc<PreparedTextLayout>) {
let drawn = DrawnLayout {
key,
prepared,
pass: self.pass,
};
if self.slots.len() < DRAWN_LAYOUTS {
self.slots.reserve_exact(DRAWN_LAYOUTS - self.slots.len());
self.slots.push(drawn);
} else if let Some(oldest) = self.slots.iter_mut().min_by_key(|slot| slot.pass) {
*oldest = drawn;
}
}
fn begin_pass(&mut self) {
self.pass += 1;
let pass = self.pass;
self.slots.retain(|slot| pass - slot.pass <= 1);
}
}
#[derive(Default)]
struct ReturningKeys {
tags: Vec<Option<NonZeroU32>>,
}
impl ReturningKeys {
fn contains(&self, hash: u64) -> bool {
let (slot, tag) = slot_and_tag(hash);
self.tags.get(slot) == Some(&Some(tag))
}
fn put(&mut self, hash: u64) {
if self.tags.is_empty() {
self.tags = vec![None; RETURNING_SLOTS];
}
let (slot, tag) = slot_and_tag(hash);
if let Some(kept) = self.tags.get_mut(slot) {
*kept = Some(tag);
}
}
}
fn slot_and_tag(hash: u64) -> (usize, NonZeroU32) {
let slot = usize::try_from(hash >> (u64::BITS - RETURNING_SLOTS.ilog2())).unwrap_or(0);
let low = u32::try_from(hash & u64::from(u32::MAX)).unwrap_or(0);
(slot, NonZeroU32::MIN | low)
}