// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Font caching primitives.
///
/// Ported from `swash/src/cache.rs` (swash is dual-licensed Apache-2.0 OR MIT).
///|
/// Uniquely generated value for identifying and caching fonts.
pub struct CacheKey {
value : UInt64
}
///|
///|
let key_counter : Ref[UInt64] = Ref((1).to_uint64())
///|
pub fn CacheKey::CacheKey() -> CacheKey {
// swash uses AtomicUsize; we only target single-thread for now.
let v = key_counter.val
key_counter.val = v + 1
CacheKey::{ value: v }
}
///|
pub fn CacheKey::value(self : CacheKey) -> UInt64 {
self.value
}
///|
/// Simple LRU cache keyed by a custom font identifier.
///
/// Ported from `swash/src/cache.rs` (`FontCache`).
pub struct FontCache[T] {
entries : Array[Entry[T]]
max_entries : Int
mut epoch : UInt64
}
///|
///|
struct Entry[T] {
mut epoch : UInt64
mut id0 : UInt64
mut id1 : UInt64
mut data : Ref[T]
}
///|
pub fn[T] FontCache::FontCache(max_entries : Int) -> FontCache[T] {
let max_entries = if max_entries < 1 { 1 } else { max_entries }
FontCache::{ entries: [], max_entries, epoch: 0 }
}
///|
fn[T] FontCache::find(
self : FontCache[T],
id0 : UInt64,
id1 : UInt64,
) -> (Bool, Int) {
let mut lowest = 0
let mut lowest_epoch = self.epoch
for i in 0.. T,
) -> ((UInt64, UInt64), Ref[T]) {
let (id0, id1) = match id_override {
None => (font.key().value(), (-1).to_int64().reinterpret_as_uint64())
Some((a, b)) => (a, b)
}
let (found, index) = self.find(id0, id1)
if found {
let e = self.entries.at(index)
e.epoch = self.epoch
self.entries.set(index, e)
((e.id0, e.id1), e.data)
} else {
self.epoch = self.epoch + 1
let data = Ref(f(font))
if index == self.entries.length() {
self.entries.push(Entry::{ epoch: self.epoch, id0, id1, data })
((id0, id1), self.entries.at(index).data)
} else {
let e = self.entries.at(index)
e.epoch = self.epoch
e.id0 = id0
e.id1 = id1
e.data = data
self.entries.set(index, e)
((id0, id1), self.entries.at(index).data)
}
}
}
///|
test "CacheKey::new produces increasing keys" {
let a = CacheKey::CacheKey().value()
let b = CacheKey::CacheKey().value()
inspect(a < b, content="true")
}
///|
test "FontCache caches and evicts by max_entries" {
// Minimal TTF header tag 0x00010000 is enough for FontRef::from_offset.
let data = Bytes::from_array([0, 1, 0, 0])
let f0 = FontRef::from_offset(data, 0).unwrap()
let f1 = FontRef::from_offset(data, 0).unwrap()
let cache : FontCache[Int] = FontCache::FontCache(1)
let calls = Ref(0)
fn mk(calls : Ref[Int], v : Int) -> (FontRef) -> Int {
fn(font) {
calls.val = calls.val + 1
// ensure the font value is used to avoid unused warnings
font.offset() |> ignore
v
}
}
let (_, r0) = cache.get(f0, None, mk(calls, 10))
let (_, r0_again) = cache.get(f0, None, mk(calls, 99))
inspect(r0.val, content="10")
inspect(r0_again.val, content="10")
inspect(calls.val, content="1")
let (_, r1) = cache.get(f1, None, mk(calls, 20))
inspect(r1.val, content="20")
inspect(calls.val, content="2")
// f0 was evicted (max_entries=1), so this will repopulate.
let (_, r0_new) = cache.get(f0, None, mk(calls, 30))
inspect(r0_new.val, content="30")
inspect(calls.val, content="3")
}