feat(unicode,datastruct): port Unicode tables and data structures
ghostty-unicode: Ghostty computes codepoint properties with the uucode Zig package (v0.2.0, Unicode 17.0.0). There is no Rust equivalent, so a trimmed UCD set is vendored in ucd/ and build.rs re-implements uucode's derivations: cell width rules incl. Ghostty's overrides for emoji modifiers and Prepend, grapheme break classes with the compile-time grapheme_break state table, the is_symbol set, and emoji presentation. Lookups are three array indexes per codepoint. Validated against the official GraphemeBreakTest.txt. ghostty-datastruct: split_tree (used for GTK splits; f16 ratios so rounding and debug diagrams match Zig), circ_buf, lru, cache_table, blocking_queue, segmented_pool, message_data, comparison, array_list_collection. intrusive_linked_list is not needed (lru is index based). lru now reports the evicted (old) key, Zig reported the new key, which appears to be a bug. Not yet covered: case_folding_full (needed by input bindings). Test Plan: - cargo test -p ghostty-unicode (16) -p ghostty-datastruct (88) - cargo clippy for both crates with -D warnings
This commit is contained in:
1 parent
b96984b020
commit
5b8c92b98c
31 files changed
+18468
-3
No files matched your search
@@ -0,0 +1,47 @@
|
||||
//! A collection of Vecs with methods for bulk operations
|
||||
//! (`array_list_collection.zig`).
|
||||
|
||||
/// A collection of Vecs with methods for bulk operations.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ArrayListCollection<T> {
|
||||
/// The lists that belong to this collection.
|
||||
pub lists: Vec<Vec<T>>,
|
||||
}
|
||||
|
||||
impl<T> ArrayListCollection<T> {
|
||||
/// The collection will be initialized with empty lists.
|
||||
#[must_use]
|
||||
pub fn new(list_count: usize, initial_capacity: usize) -> Self {
|
||||
Self {
|
||||
lists: (0..list_count)
|
||||
.map(|_| Vec::with_capacity(initial_capacity))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all lists in the collection, retaining capacity.
|
||||
pub fn reset(&mut self) {
|
||||
for list in &mut self.lists {
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn init_and_reset() {
|
||||
let mut c: ArrayListCollection<u32> = ArrayListCollection::new(3, 8);
|
||||
assert_eq!(c.lists.len(), 3);
|
||||
for l in &c.lists {
|
||||
assert!(l.is_empty());
|
||||
assert!(l.capacity() >= 8);
|
||||
}
|
||||
c.lists[1].extend([1, 2, 3]);
|
||||
c.reset();
|
||||
assert!(c.lists.iter().all(Vec::is_empty));
|
||||
assert!(c.lists[1].capacity() >= 8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
//! Blocking queue implementation aimed primarily for message passing
|
||||
//! between threads (`blocking_queue.zig`).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Condvar, Mutex, MutexGuard, PoisonError};
|
||||
use std::time::Duration;
|
||||
|
||||
/// The type we use for queue size types.
|
||||
pub type Size = u32;
|
||||
|
||||
/// Specifies the timeout for an operation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Timeout {
|
||||
/// Fail instantly (non-blocking).
|
||||
Instant,
|
||||
|
||||
/// Run forever or until interrupted
|
||||
Forever,
|
||||
|
||||
/// Nanoseconds
|
||||
Ns(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner<T> {
|
||||
data: VecDeque<T>,
|
||||
not_full_waiters: usize,
|
||||
}
|
||||
|
||||
/// A blocking queue for type T with a fixed `CAPACITY`.
|
||||
///
|
||||
/// This is tailor made for ghostty usage so it isn't meant to be maximally
|
||||
/// generic. Traits of this queue that are specific to our usage:
|
||||
///
|
||||
/// - Fixed size. We expect our queue to quickly drain and also not be
|
||||
/// too large so we prefer a fixed size queue for now.
|
||||
/// - No blocking pop. We use an external event loop mechanism such as
|
||||
/// eventfd to notify our waiter that there is no data available so
|
||||
/// we don't need to implement a blocking pop.
|
||||
/// - Drain function. Most queues usually pop one at a time. We have
|
||||
/// a mechanism for draining since on every IO loop our TTY drains
|
||||
/// the full queue so we can get rid of the overhead of a ton of
|
||||
/// locks and bounds checking and do a one-time drain.
|
||||
///
|
||||
/// One key usage pattern is that our blocking queues are single producer
|
||||
/// single consumer (SPSC). This should let us do some interesting optimizations
|
||||
/// in the future. At the time of writing this, the blocking queue implementation
|
||||
/// is purposely naive to build something quickly, but we should benchmark
|
||||
/// and make this more optimized as necessary.
|
||||
#[derive(Debug)]
|
||||
pub struct BlockingQueue<T, const CAPACITY: usize> {
|
||||
/// The big mutex that must be held to read/write.
|
||||
inner: Mutex<Inner<T>>,
|
||||
|
||||
/// A CV for being notified when the queue is no longer full. This is
|
||||
/// used for writing. Note we DON'T have a CV for waiting on the
|
||||
/// queue not being EMPTY because we use external notifiers for that.
|
||||
cond_not_full: Condvar,
|
||||
}
|
||||
|
||||
impl<T, const CAPACITY: usize> Default for BlockingQueue<T, CAPACITY> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const CAPACITY: usize> BlockingQueue<T, CAPACITY> {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Mutex::new(Inner {
|
||||
data: VecDeque::with_capacity(CAPACITY),
|
||||
not_full_waiters: 0,
|
||||
}),
|
||||
cond_not_full: Condvar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lock(&self) -> MutexGuard<'_, Inner<T>> {
|
||||
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Push a value to the queue. This returns the total size of the
|
||||
/// queue (unread items) after the push. A return value of zero
|
||||
/// means that the push failed.
|
||||
pub fn push(&self, value: T, timeout: Timeout) -> Size {
|
||||
let mut g = self.lock();
|
||||
|
||||
if g.data.len() >= CAPACITY {
|
||||
match timeout {
|
||||
// If we're not waiting, then we failed to write.
|
||||
Timeout::Instant => return 0,
|
||||
|
||||
Timeout::Forever => {
|
||||
g.not_full_waiters += 1;
|
||||
g = self
|
||||
.cond_not_full
|
||||
.wait_while(g, |i| i.data.len() >= CAPACITY)
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
g.not_full_waiters -= 1;
|
||||
}
|
||||
|
||||
Timeout::Ns(ns) => {
|
||||
g.not_full_waiters += 1;
|
||||
let (guard, _timed_out) = self
|
||||
.cond_not_full
|
||||
.wait_timeout_while(g, Duration::from_nanos(ns), |i| {
|
||||
i.data.len() >= CAPACITY
|
||||
})
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
g = guard;
|
||||
g.not_full_waiters -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're still full, then we failed to write. This can
|
||||
// happen in situations where we are interrupted.
|
||||
if g.data.len() >= CAPACITY {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add our data and update our accounting
|
||||
g.data.push_back(value);
|
||||
g.data.len() as Size
|
||||
}
|
||||
|
||||
/// Pop a value from the queue without blocking.
|
||||
pub fn pop(&self) -> Option<T> {
|
||||
let mut g = self.lock();
|
||||
|
||||
// If we're empty we have nothing
|
||||
let v = g.data.pop_front()?;
|
||||
|
||||
// If we have producers waiting on a full queue, notify.
|
||||
if g.not_full_waiters > 0 {
|
||||
self.cond_not_full.notify_one();
|
||||
}
|
||||
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// Pop all values from the queue. This will hold the big mutex
|
||||
/// until the returned iterator is dropped. This is used if
|
||||
/// you know you're going to "pop" and utilize all the values
|
||||
/// quickly to avoid many locks, bounds checks, and cv signals.
|
||||
pub fn drain(&self) -> DrainIterator<'_, T, CAPACITY> {
|
||||
DrainIterator {
|
||||
guard: self.lock(),
|
||||
queue: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator returned by [`BlockingQueue::drain`]. Holds the queue lock until
|
||||
/// dropped (the Zig `deinit`).
|
||||
pub struct DrainIterator<'a, T, const CAPACITY: usize> {
|
||||
guard: MutexGuard<'a, Inner<T>>,
|
||||
queue: &'a BlockingQueue<T, CAPACITY>,
|
||||
}
|
||||
|
||||
impl<T, const CAPACITY: usize> Iterator for DrainIterator<'_, T, CAPACITY> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<T> {
|
||||
self.guard.data.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const CAPACITY: usize> Drop for DrainIterator<'_, T, CAPACITY> {
|
||||
fn drop(&mut self) {
|
||||
// If we have producers waiting on a full queue, notify. The lock is
|
||||
// released after this when the guard is dropped.
|
||||
if self.guard.not_full_waiters > 0 {
|
||||
self.queue.cond_not_full.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn basic_push_and_pop() {
|
||||
let q = BlockingQueue::<u64, 4>::new();
|
||||
|
||||
// Should have no values
|
||||
assert!(q.pop().is_none());
|
||||
|
||||
// Push until we're full
|
||||
assert_eq!(q.push(1, Timeout::Instant), 1);
|
||||
assert_eq!(q.push(2, Timeout::Instant), 2);
|
||||
assert_eq!(q.push(3, Timeout::Instant), 3);
|
||||
assert_eq!(q.push(4, Timeout::Instant), 4);
|
||||
assert_eq!(q.push(5, Timeout::Instant), 0);
|
||||
|
||||
// Pop!
|
||||
assert_eq!(q.pop().unwrap(), 1);
|
||||
assert_eq!(q.pop().unwrap(), 2);
|
||||
assert_eq!(q.pop().unwrap(), 3);
|
||||
assert_eq!(q.pop().unwrap(), 4);
|
||||
assert!(q.pop().is_none());
|
||||
|
||||
// Drain does nothing
|
||||
let mut it = q.drain();
|
||||
assert!(it.next().is_none());
|
||||
drop(it);
|
||||
|
||||
// Verify we can still push
|
||||
assert_eq!(q.push(1, Timeout::Instant), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timed_push() {
|
||||
let q = BlockingQueue::<u64, 1>::new();
|
||||
|
||||
// Push
|
||||
assert_eq!(q.push(1, Timeout::Instant), 1);
|
||||
assert_eq!(q.push(2, Timeout::Instant), 0);
|
||||
|
||||
// Timed push should fail
|
||||
assert_eq!(q.push(2, Timeout::Ns(1000)), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_returns_all_in_order() {
|
||||
let q = BlockingQueue::<u8, 8>::new();
|
||||
for i in 0..5 {
|
||||
q.push(i, Timeout::Instant);
|
||||
}
|
||||
let v: Vec<u8> = q.drain().collect();
|
||||
assert_eq!(v, [0, 1, 2, 3, 4]);
|
||||
assert!(q.pop().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_push_wakes_on_pop() {
|
||||
let q = Arc::new(BlockingQueue::<u32, 1>::new());
|
||||
assert_eq!(q.push(1, Timeout::Instant), 1);
|
||||
|
||||
let q2 = Arc::clone(&q);
|
||||
let t = thread::spawn(move || q2.push(2, Timeout::Forever));
|
||||
|
||||
// Wait until the producer is blocked, then pop.
|
||||
loop {
|
||||
if q.lock().not_full_waiters > 0 {
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
assert_eq!(q.pop(), Some(1));
|
||||
assert_eq!(t.join().unwrap(), 1);
|
||||
assert_eq!(q.pop(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_push_wakes_on_drain() {
|
||||
let q = Arc::new(BlockingQueue::<u32, 1>::new());
|
||||
assert_eq!(q.push(1, Timeout::Instant), 1);
|
||||
|
||||
let q2 = Arc::clone(&q);
|
||||
let t = thread::spawn(move || q2.push(2, Timeout::Ns(5_000_000_000)));
|
||||
loop {
|
||||
if q.lock().not_full_waiters > 0 {
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
drop(q.drain().collect::<Vec<_>>());
|
||||
assert_eq!(t.join().unwrap(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! A fixed-size-bucket hash cache with LRU replacement (`cache_table.zig`).
|
||||
|
||||
/// Behaviours for a [`CacheTable`] (the Zig `Context`).
|
||||
pub trait CacheContext<K, V> {
|
||||
/// Return a hash for a key.
|
||||
fn hash(&mut self, key: &K) -> u64;
|
||||
|
||||
/// Check two keys for equality.
|
||||
fn eql(&mut self, a: &K, b: &K) -> bool;
|
||||
|
||||
/// [OPTIONAL] Eviction callback. Called whenever an item is evicted
|
||||
/// from the cache.
|
||||
fn evicted(&mut self, _key: K, _value: V) {}
|
||||
}
|
||||
|
||||
/// A key/value pair stored in the cache.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Kv<K, V> {
|
||||
pub key: K,
|
||||
pub value: V,
|
||||
}
|
||||
|
||||
/// An associative data structure used for efficiently storing and
|
||||
/// retrieving values which are able to be recomputed if necessary.
|
||||
///
|
||||
/// This structure is effectively a hash table with fixed-sized buckets.
|
||||
///
|
||||
/// When inserting an item in to a full bucket, the least recently used
|
||||
/// item is replaced.
|
||||
///
|
||||
/// To achieve this, when an item is accessed, it's moved to the end of
|
||||
/// the bucket, and the rest of the items are moved over to fill the gap.
|
||||
///
|
||||
/// This should provide very good query performance and keep frequently
|
||||
/// accessed items cached indefinitely.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// `C`
|
||||
/// A [`CacheContext`] defining hash/eql (and optionally an eviction hook).
|
||||
///
|
||||
/// `BUCKET_COUNT`
|
||||
/// Should ideally be close to the median number of important items that
|
||||
/// you expect to be cached at any given point. This is required to be a
|
||||
/// power of 2 since performance suffers if it's not and there's no good
|
||||
/// reason to allow it to be anything else.
|
||||
///
|
||||
/// `BUCKET_SIZE`
|
||||
/// should be larger if you expect a large number of unimportant items to
|
||||
/// enter the cache at a time. Having larger buckets will avoid important
|
||||
/// items being dropped from the cache prematurely.
|
||||
#[derive(Debug)]
|
||||
pub struct CacheTable<K, V, C, const BUCKET_COUNT: usize, const BUCKET_SIZE: usize> {
|
||||
/// `BUCKET_COUNT` buckets containing up to `BUCKET_SIZE` KV pairs each;
|
||||
/// oldest first.
|
||||
buckets: Vec<Vec<Kv<K, V>>>,
|
||||
|
||||
/// An instance of the context structure.
|
||||
pub context: C,
|
||||
}
|
||||
|
||||
impl<K, V, C, const BUCKET_COUNT: usize, const BUCKET_SIZE: usize>
|
||||
CacheTable<K, V, C, BUCKET_COUNT, BUCKET_SIZE>
|
||||
where
|
||||
C: CacheContext<K, V>,
|
||||
{
|
||||
const ASSERT_POW2: () = assert!(BUCKET_COUNT.is_power_of_two());
|
||||
const ASSERT_SIZE: () = assert!(BUCKET_SIZE > 0 && BUCKET_SIZE <= u8::MAX as usize);
|
||||
|
||||
#[must_use]
|
||||
pub fn new(context: C) -> Self {
|
||||
let () = Self::ASSERT_POW2;
|
||||
let () = Self::ASSERT_SIZE;
|
||||
let mut buckets = Vec::with_capacity(BUCKET_COUNT);
|
||||
buckets.resize_with(BUCKET_COUNT, Vec::new);
|
||||
Self { buckets, context }
|
||||
}
|
||||
|
||||
fn index(&mut self, key: &K) -> usize {
|
||||
(self.context.hash(key) % BUCKET_COUNT as u64) as usize
|
||||
}
|
||||
|
||||
/// Adds an item to the cache table. If an old value was removed to
|
||||
/// make room then it is returned in a struct with its key and value.
|
||||
pub fn put(&mut self, key: K, value: V) -> Option<Kv<K, V>>
|
||||
where
|
||||
K: Clone,
|
||||
V: Clone,
|
||||
{
|
||||
let idx = self.index(&key);
|
||||
let bucket = &mut self.buckets[idx];
|
||||
|
||||
// If we have space available in the bucket then we just append
|
||||
if bucket.len() < BUCKET_SIZE {
|
||||
bucket.push(Kv { key, value });
|
||||
return None;
|
||||
}
|
||||
debug_assert_eq!(bucket.len(), BUCKET_SIZE);
|
||||
|
||||
// Append our new item and return the oldest
|
||||
let evicted = bucket.remove(0);
|
||||
bucket.push(Kv { key, value });
|
||||
|
||||
// The Context is allowed to register an eviction hook.
|
||||
self.context
|
||||
.evicted(evicted.key.clone(), evicted.value.clone());
|
||||
|
||||
Some(evicted)
|
||||
}
|
||||
|
||||
/// Retrieves an item from the cache table (marking it most recently
|
||||
/// used).
|
||||
///
|
||||
/// Returns `None` if no item is found with the provided key.
|
||||
pub fn get(&mut self, key: &K) -> Option<&V> {
|
||||
let idx = self.index(key);
|
||||
let len = self.buckets[idx].len();
|
||||
let mut i = len;
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
if self.context.eql(key, &self.buckets[idx][i].key) {
|
||||
// Rotate the item to the end of the bucket.
|
||||
self.buckets[idx][i..len].rotate_left(1);
|
||||
return Some(&self.buckets[idx][len - 1].value);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Removes all items from the cache table.
|
||||
///
|
||||
/// If your `Context` has an `evicted` method,
|
||||
/// it will be called with all removed items.
|
||||
pub fn clear(&mut self) {
|
||||
for b in &mut self.buckets {
|
||||
for kv in b.drain(..) {
|
||||
self.context.evicted(kv.key, kv.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A context for any `K: Hash + Eq` using the std default hasher (the Zig
|
||||
/// `AutoContext`).
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct AutoContext;
|
||||
|
||||
impl<K: std::hash::Hash + Eq, V> CacheContext<K, V> for AutoContext {
|
||||
fn hash(&mut self, key: &K) -> u64 {
|
||||
use std::hash::Hasher;
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
key.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn eql(&mut self, a: &K, b: &K) -> bool {
|
||||
a == b
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// A context that has a predictable hash so we can test all edge cases.
|
||||
#[derive(Default)]
|
||||
struct Ctx {
|
||||
evicted: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl CacheContext<u32, u32> for Ctx {
|
||||
fn hash(&mut self, key: &u32) -> u64 {
|
||||
u64::from(*key)
|
||||
}
|
||||
fn eql(&mut self, a: &u32, b: &u32) -> bool {
|
||||
a == b
|
||||
}
|
||||
fn evicted(&mut self, key: u32, value: u32) {
|
||||
self.evicted.push((key, value));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_table() {
|
||||
let mut t: CacheTable<u32, u32, Ctx, 2, 2> = CacheTable::new(Ctx::default());
|
||||
|
||||
// Fill the table
|
||||
assert!(t.put(0, 0).is_none());
|
||||
assert!(t.put(1, 0).is_none());
|
||||
assert!(t.put(2, 0).is_none());
|
||||
assert!(t.put(3, 0).is_none());
|
||||
|
||||
// It should now be full, so any insert should evict the oldest item.
|
||||
// NOTE: For the sake of this test, we're assuming that the first item
|
||||
// is evicted but we don't need to promise this.
|
||||
assert_eq!(t.put(4, 0).unwrap(), Kv { key: 0, value: 0 });
|
||||
|
||||
// The first item should now be gone
|
||||
assert!(t.get(&0).is_none());
|
||||
assert_eq!(t.context.evicted, vec![(0, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_table_get_refreshes_and_clear() {
|
||||
let mut t: CacheTable<u32, u32, Ctx, 1, 3> = CacheTable::new(Ctx::default());
|
||||
t.put(1, 10);
|
||||
t.put(2, 20);
|
||||
t.put(3, 30);
|
||||
// Touch 1 so 2 becomes the oldest.
|
||||
assert_eq!(t.get(&1), Some(&10));
|
||||
assert_eq!(t.put(4, 40).unwrap().key, 2);
|
||||
assert_eq!(t.get(&3), Some(&30));
|
||||
t.clear();
|
||||
assert!(t.get(&1).is_none());
|
||||
assert_eq!(t.context.evicted.len(), 1 + 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_table_auto_context() {
|
||||
let mut t: CacheTable<String, u8, AutoContext, 4, 2> = CacheTable::new(AutoContext);
|
||||
t.put("a".to_string(), 1);
|
||||
assert_eq!(t.get(&"a".to_string()), Some(&1));
|
||||
assert_eq!(t.get(&"b".to_string()), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
//! A circular buffer (`circ_buf.zig`).
|
||||
|
||||
use crate::OutOfMemory;
|
||||
|
||||
/// Direction of a [`Iter`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Forward,
|
||||
Reverse,
|
||||
}
|
||||
|
||||
/// A circular buffer containing type `T`, filled with a `default` value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CircBuf<T> {
|
||||
// Implementation note: there's a lot of unsafe addition of usize
|
||||
// here in this implementation that can technically overflow. If someone
|
||||
// wants to fix this and make it overflow safe (use subtractions for
|
||||
// checks prior to additions) then I welcome it. In reality, we'd
|
||||
// have to be a really, really large terminal screen to even worry
|
||||
// about this so I'm punting it.
|
||||
pub storage: Vec<T>,
|
||||
pub head: usize,
|
||||
pub tail: usize,
|
||||
|
||||
// We could remove this and just use math with head/tail to figure
|
||||
// it out, but our usage of circular buffers stores so much data that
|
||||
// this minor overhead is not worth optimizing out.
|
||||
pub full: bool,
|
||||
|
||||
default: T,
|
||||
}
|
||||
|
||||
/// Iterator over the circular buffer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Iter<'a, T> {
|
||||
buf: &'a CircBuf<T>,
|
||||
idx: usize,
|
||||
direction: Direction,
|
||||
}
|
||||
|
||||
impl<T> Iter<'_, T> {
|
||||
/// Seek the iterator by a given amount. This will clamp
|
||||
/// the values to the bounds of the buffer so overflows are
|
||||
/// not possible.
|
||||
pub fn seek_by(&mut self, amount: isize) {
|
||||
if amount > 0 {
|
||||
self.idx = self.idx.saturating_add(amount as usize);
|
||||
} else {
|
||||
self.idx = self.idx.saturating_sub(amount.unsigned_abs());
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the iterator back to the first value.
|
||||
pub fn reset(&mut self) {
|
||||
self.idx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for Iter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<&'a T> {
|
||||
if self.idx >= self.buf.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get our index from the tail
|
||||
let tail_idx = match self.direction {
|
||||
Direction::Forward => self.idx,
|
||||
Direction::Reverse => self.buf.len() - self.idx - 1,
|
||||
};
|
||||
|
||||
// Translate the tail index to a storage index
|
||||
let storage_idx = (self.buf.tail + tail_idx) % self.buf.capacity();
|
||||
self.idx += 1;
|
||||
Some(&self.buf.storage[storage_idx])
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CircBuf<T> {
|
||||
/// Returns if the buffer is currently empty. To check if its
|
||||
/// full, just check the "full" attribute.
|
||||
#[must_use]
|
||||
pub fn empty(&self) -> bool {
|
||||
!self.full && self.head == self.tail
|
||||
}
|
||||
|
||||
/// Returns the total capacity allocated for this buffer.
|
||||
#[must_use]
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.storage.len()
|
||||
}
|
||||
|
||||
/// Returns the length in elements that are used.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
if self.full {
|
||||
return self.storage.len();
|
||||
}
|
||||
if self.head >= self.tail {
|
||||
return self.head - self.tail;
|
||||
}
|
||||
self.storage.len() - (self.tail - self.head)
|
||||
}
|
||||
|
||||
/// Alias for [`empty`](Self::empty) (the clippy-conventional name).
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> CircBuf<T> {
|
||||
/// Initialize a new circular buffer that can store `size` elements.
|
||||
#[must_use]
|
||||
pub fn new(size: usize, default: T) -> Self {
|
||||
Self {
|
||||
storage: vec![default.clone(); size],
|
||||
head: 0,
|
||||
tail: 0,
|
||||
full: size == 0,
|
||||
default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a single value to the buffer. If the buffer is full,
|
||||
/// an error will be returned.
|
||||
pub fn append(&mut self, v: T) -> Result<(), OutOfMemory> {
|
||||
if self.full {
|
||||
return Err(OutOfMemory);
|
||||
}
|
||||
self.storage[self.head] = v;
|
||||
self.head += 1;
|
||||
if self.head >= self.storage.len() {
|
||||
self.head = 0;
|
||||
}
|
||||
self.full = self.head == self.tail;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Append a single value to the buffer, assuming there is capacity.
|
||||
pub fn append_assume_capacity(&mut self, v: T) {
|
||||
assert!(!self.full);
|
||||
self.storage[self.head] = v;
|
||||
self.head += 1;
|
||||
if self.head >= self.storage.len() {
|
||||
self.head = 0;
|
||||
}
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// Append a slice to the buffer.
|
||||
pub fn append_slice_assume_capacity(&mut self, slice: &[T]) {
|
||||
let len = self.len();
|
||||
let [a, b] = self.get_ptr_slice(len, slice.len());
|
||||
let a_len = a.len();
|
||||
a.clone_from_slice(&slice[..a_len]);
|
||||
b.clone_from_slice(&slice[a_len..]);
|
||||
}
|
||||
|
||||
/// Clear the buffer.
|
||||
pub fn clear(&mut self) {
|
||||
self.head = 0;
|
||||
self.tail = 0;
|
||||
self.full = false;
|
||||
}
|
||||
|
||||
/// Iterate over the circular buffer.
|
||||
#[must_use]
|
||||
pub fn iterator(&self, direction: Direction) -> Iter<'_, T> {
|
||||
Iter {
|
||||
buf: self,
|
||||
idx: 0,
|
||||
direction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first (oldest) value in the buffer.
|
||||
#[must_use]
|
||||
pub fn first(&self) -> Option<&T> {
|
||||
self.iterator(Direction::Forward).next()
|
||||
}
|
||||
|
||||
/// Get the last (newest) value in the buffer.
|
||||
#[must_use]
|
||||
pub fn last(&self) -> Option<&T> {
|
||||
self.iterator(Direction::Reverse).next()
|
||||
}
|
||||
|
||||
/// Mutable access to the first (oldest) value in the buffer.
|
||||
pub fn first_mut(&mut self) -> Option<&mut T> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let idx = self.tail % self.capacity();
|
||||
Some(&mut self.storage[idx])
|
||||
}
|
||||
|
||||
/// Mutable access to the last (newest) value in the buffer.
|
||||
pub fn last_mut(&mut self) -> Option<&mut T> {
|
||||
let len = self.len();
|
||||
if len == 0 {
|
||||
return None;
|
||||
}
|
||||
let idx = (self.tail + len - 1) % self.capacity();
|
||||
Some(&mut self.storage[idx])
|
||||
}
|
||||
|
||||
/// Ensures that there is enough capacity to store `amount` more
|
||||
/// items via append.
|
||||
pub fn ensure_unused_capacity(&mut self, amount: usize) {
|
||||
let new_cap = self.len() + amount;
|
||||
if new_cap <= self.capacity() {
|
||||
return;
|
||||
}
|
||||
self.resize(new_cap);
|
||||
}
|
||||
|
||||
/// Resize the buffer to the given size (larger or smaller).
|
||||
/// If larger, new values will be set to the default value.
|
||||
pub fn resize(&mut self, size: usize) {
|
||||
// Rotate to zero so it is aligned.
|
||||
self.rotate_to_zero();
|
||||
|
||||
// Reallocate, this adds to the end so we're ready to go.
|
||||
let prev_len = self.len();
|
||||
let prev_cap = self.storage.len();
|
||||
self.storage.resize(size, self.default.clone());
|
||||
|
||||
// If we grew, we need to set our new defaults. We can add it
|
||||
// at the end since we rotated to start. (Done by `resize` above.)
|
||||
if size > prev_cap {
|
||||
// Fix up our head/tail
|
||||
if self.full {
|
||||
self.head = prev_len;
|
||||
self.full = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate the data so that it is zero-aligned.
|
||||
pub(crate) fn rotate_to_zero(&mut self) {
|
||||
// If we're already at zero then do nothing.
|
||||
if self.tail == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Rotate our storage in-place.
|
||||
self.storage.rotate_left(self.tail);
|
||||
|
||||
// Then fix up our head and tail.
|
||||
self.head = self.len() % self.storage.len();
|
||||
self.tail = 0;
|
||||
}
|
||||
|
||||
/// Delete the oldest n values from the buffer. If there are less
|
||||
/// than n values in the buffer, it'll delete everything.
|
||||
pub fn delete_oldest(&mut self, n: usize) {
|
||||
assert!(n <= self.storage.len());
|
||||
|
||||
// Special case n == 0 otherwise we will accidentally break
|
||||
// our circular buffer.
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the values back to default
|
||||
let default = self.default.clone();
|
||||
let [a, b] = self.get_ptr_slice(0, n);
|
||||
a.fill(default.clone());
|
||||
b.fill(default);
|
||||
|
||||
// If we're not full, we can just advance the tail. We know
|
||||
// it'll be less than the length because otherwise we'd be full.
|
||||
self.tail += self.len().min(n);
|
||||
if self.tail >= self.storage.len() {
|
||||
self.tail -= self.storage.len();
|
||||
}
|
||||
self.full = false;
|
||||
}
|
||||
|
||||
/// Returns the two (possibly empty) slices covering `slice_len` values at
|
||||
/// `offset`, and considers this full amount of data "written" if it is
|
||||
/// beyond the end of our buffer. This never "rotates" the buffer because
|
||||
/// the offset can only be within the size of the buffer.
|
||||
pub fn get_ptr_slice(&mut self, offset: usize, slice_len: usize) -> [&mut [T]; 2] {
|
||||
// Special case the empty slice fast-path.
|
||||
if slice_len == 0 {
|
||||
return [&mut [], &mut []];
|
||||
}
|
||||
|
||||
assert!(offset + slice_len <= self.capacity());
|
||||
|
||||
// End offset is the last offset (exclusive) for our slice.
|
||||
// We use exclusive because it makes the math easier.
|
||||
let end_offset = offset + slice_len;
|
||||
|
||||
// If our slice can't fit it in our length, then we need to advance.
|
||||
if end_offset > self.len() {
|
||||
self.advance(end_offset - self.len());
|
||||
}
|
||||
|
||||
// Our start and end indexes into the storage buffer
|
||||
let start_idx = self.storage_offset(offset);
|
||||
let end_idx = self.storage_offset(end_offset - 1);
|
||||
|
||||
// Optimistically, our data fits in one slice
|
||||
if end_idx >= start_idx {
|
||||
return [&mut self.storage[start_idx..=end_idx], &mut []];
|
||||
}
|
||||
|
||||
let (front, back) = self.storage.split_at_mut(start_idx);
|
||||
[back, &mut front[..=end_idx]]
|
||||
}
|
||||
|
||||
/// Advances the head/tail so that we can store amount.
|
||||
fn advance(&mut self, amount: usize) {
|
||||
assert!(amount <= self.storage.len() - self.len());
|
||||
|
||||
// Optimistically add our amount
|
||||
self.head += amount;
|
||||
|
||||
// If we exceeded the length of the buffer, wrap around.
|
||||
if self.head >= self.storage.len() {
|
||||
self.head -= self.storage.len();
|
||||
}
|
||||
|
||||
// If we're full, we have to keep tail lined up.
|
||||
if self.full {
|
||||
self.tail = self.head;
|
||||
}
|
||||
|
||||
// We're full if the head reached the tail. The head can never
|
||||
// pass the tail because advance asserts amount is only in
|
||||
// available space left
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// For a given offset from zero, this returns the offset in the
|
||||
/// storage buffer where this data can be found.
|
||||
fn storage_offset(&self, offset: usize) -> usize {
|
||||
assert!(offset < self.storage.len());
|
||||
|
||||
// This should be subtraction ideally to avoid overflows but
|
||||
// it would take a really, really, huge buffer to overflow.
|
||||
let fits_offset = self.tail + offset;
|
||||
if fits_offset < self.storage.len() {
|
||||
return fits_offset;
|
||||
}
|
||||
fits_offset - self.storage.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn collect(buf: &CircBuf<u8>, d: Direction) -> Vec<u8> {
|
||||
buf.iterator(d).copied().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_basic() {
|
||||
let buf = CircBuf::new(12, 0u8);
|
||||
|
||||
assert!(buf.empty());
|
||||
assert_eq!(buf.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_append() {
|
||||
let mut buf = CircBuf::new(3, 0u8);
|
||||
|
||||
buf.append(1).unwrap();
|
||||
buf.append(2).unwrap();
|
||||
buf.append(3).unwrap();
|
||||
assert_eq!(buf.append(4), Err(OutOfMemory));
|
||||
buf.delete_oldest(1);
|
||||
buf.append(4).unwrap();
|
||||
assert_eq!(buf.append(5), Err(OutOfMemory));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_forward_iterator() {
|
||||
let mut buf = CircBuf::new(3, 0u8);
|
||||
|
||||
// Empty
|
||||
assert!(buf.iterator(Direction::Forward).next().is_none());
|
||||
|
||||
// Partially full
|
||||
buf.append(1).unwrap();
|
||||
buf.append(2).unwrap();
|
||||
assert_eq!(collect(&buf, Direction::Forward), [1, 2]);
|
||||
|
||||
// Full
|
||||
buf.append(3).unwrap();
|
||||
assert_eq!(collect(&buf, Direction::Forward), [1, 2, 3]);
|
||||
|
||||
// Delete and add
|
||||
buf.delete_oldest(1);
|
||||
buf.append(4).unwrap();
|
||||
assert_eq!(collect(&buf, Direction::Forward), [2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_reverse_iterator() {
|
||||
let mut buf = CircBuf::new(3, 0u8);
|
||||
|
||||
// Empty
|
||||
assert!(buf.iterator(Direction::Reverse).next().is_none());
|
||||
|
||||
// Partially full
|
||||
buf.append(1).unwrap();
|
||||
buf.append(2).unwrap();
|
||||
assert_eq!(collect(&buf, Direction::Reverse), [2, 1]);
|
||||
|
||||
// Full
|
||||
buf.append(3).unwrap();
|
||||
assert_eq!(collect(&buf, Direction::Reverse), [3, 2, 1]);
|
||||
|
||||
// Delete and add
|
||||
buf.delete_oldest(1);
|
||||
buf.append(4).unwrap();
|
||||
assert_eq!(collect(&buf, Direction::Reverse), [4, 3, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_iterator_seek() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
buf.append_slice_assume_capacity(&[1, 2, 3, 4]);
|
||||
let mut it = buf.iterator(Direction::Forward);
|
||||
it.seek_by(2);
|
||||
assert_eq!(it.next(), Some(&3));
|
||||
it.seek_by(-100);
|
||||
assert_eq!(it.next(), Some(&1));
|
||||
it.seek_by(isize::MAX);
|
||||
assert_eq!(it.next(), None);
|
||||
it.reset();
|
||||
assert_eq!(it.next(), Some(&1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_first_last() {
|
||||
let mut buf = CircBuf::new(3, 0u8);
|
||||
|
||||
buf.append(1).unwrap();
|
||||
buf.append(2).unwrap();
|
||||
buf.append(3).unwrap();
|
||||
assert_eq!(buf.last(), Some(&3));
|
||||
assert_eq!(buf.first(), Some(&1));
|
||||
*buf.last_mut().unwrap() = 9;
|
||||
*buf.first_mut().unwrap() = 7;
|
||||
assert_eq!(collect(&buf, Direction::Forward), [7, 2, 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_first_last_empty() {
|
||||
let buf = CircBuf::new(0, 0u8);
|
||||
|
||||
assert!(buf.first().is_none());
|
||||
assert!(buf.last().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_first_last_empty_with_cap() {
|
||||
let mut buf = CircBuf::new(3, 0u8);
|
||||
|
||||
assert!(buf.first().is_none());
|
||||
assert!(buf.last().is_none());
|
||||
assert!(buf.first_mut().is_none());
|
||||
assert!(buf.last_mut().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_append_slice() {
|
||||
let mut buf = CircBuf::new(5, 0u8);
|
||||
|
||||
buf.append_slice_assume_capacity(b"hello");
|
||||
assert_eq!(collect(&buf, Direction::Forward), b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_append_slice_with_wrap() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill the buffer
|
||||
let cap = buf.capacity();
|
||||
let _ = buf.get_ptr_slice(0, cap);
|
||||
assert!(buf.full);
|
||||
assert_eq!(buf.len(), 4);
|
||||
|
||||
// Delete
|
||||
buf.delete_oldest(2);
|
||||
assert!(!buf.full);
|
||||
assert_eq!(buf.len(), 2);
|
||||
|
||||
buf.append_slice_assume_capacity(b"AB");
|
||||
assert_eq!(collect(&buf, Direction::Forward), [0, 0, b'A', b'B']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_get_ptr_slice_fits() {
|
||||
let mut buf = CircBuf::new(12, 0u8);
|
||||
|
||||
let slices = buf.get_ptr_slice(0, 11);
|
||||
assert_eq!(slices[0].len(), 11);
|
||||
assert_eq!(slices[1].len(), 0);
|
||||
assert_eq!(buf.len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_get_ptr_slice_wraps() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill the buffer
|
||||
let cap = buf.capacity();
|
||||
let _ = buf.get_ptr_slice(0, cap);
|
||||
assert!(buf.full);
|
||||
assert_eq!(buf.len(), 4);
|
||||
|
||||
// Delete
|
||||
buf.delete_oldest(2);
|
||||
assert!(!buf.full);
|
||||
assert_eq!(buf.len(), 2);
|
||||
|
||||
// Get a slice that doesn't grow
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 2);
|
||||
assert_eq!(slices[0].len(), 2);
|
||||
assert_eq!(slices[1].len(), 0);
|
||||
slices[0][0] = 1;
|
||||
slices[0][1] = 2;
|
||||
}
|
||||
assert_eq!(buf.len(), 2);
|
||||
|
||||
// Get a slice that does grow, and forces wrap
|
||||
{
|
||||
let slices = buf.get_ptr_slice(2, 2);
|
||||
assert_eq!(slices[0].len(), 2);
|
||||
assert_eq!(slices[1].len(), 0);
|
||||
|
||||
// should be empty
|
||||
assert_eq!(slices[0][0], 0);
|
||||
assert_eq!(slices[0][1], 0);
|
||||
slices[0][0] = 3;
|
||||
slices[0][1] = 4;
|
||||
}
|
||||
assert_eq!(buf.len(), 4);
|
||||
|
||||
// Get a slice across boundaries
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 4);
|
||||
assert_eq!(slices[0].len(), 2);
|
||||
assert_eq!(slices[1].len(), 2);
|
||||
|
||||
assert_eq!(slices[0][0], 1);
|
||||
assert_eq!(slices[0][1], 2);
|
||||
assert_eq!(slices[1][0], 3);
|
||||
assert_eq!(slices[1][1], 4);
|
||||
}
|
||||
assert_eq!(buf.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_rotate_to_zero() {
|
||||
let mut buf = CircBuf::new(12, 0u8);
|
||||
|
||||
let _ = buf.get_ptr_slice(0, 11);
|
||||
buf.rotate_to_zero();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_rotate_to_zero_offset() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill the buffer
|
||||
let _ = buf.get_ptr_slice(0, 3);
|
||||
assert_eq!(buf.len(), 3);
|
||||
|
||||
// Delete
|
||||
buf.delete_oldest(2);
|
||||
assert!(!buf.full);
|
||||
assert_eq!(buf.len(), 1);
|
||||
assert!(buf.tail > 0 && buf.head >= buf.tail);
|
||||
|
||||
// Rotate to zero
|
||||
buf.rotate_to_zero();
|
||||
assert_eq!(buf.tail, 0);
|
||||
assert_eq!(buf.head, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_rotate_to_zero_wraps() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill the buffer
|
||||
let _ = buf.get_ptr_slice(0, 3);
|
||||
assert_eq!(buf.len(), 3);
|
||||
assert!(buf.tail == 0 && buf.head == 3);
|
||||
|
||||
// Delete all
|
||||
buf.delete_oldest(3);
|
||||
assert_eq!(buf.len(), 0);
|
||||
assert!(buf.tail == 3 && buf.head == 3);
|
||||
|
||||
// Refill to force a wrap
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 3);
|
||||
slices[0][0] = 1;
|
||||
slices[1][0] = 2;
|
||||
slices[1][1] = 3;
|
||||
}
|
||||
assert_eq!(buf.len(), 3);
|
||||
assert!(buf.tail == 3 && buf.head == 2);
|
||||
|
||||
// Rotate to zero
|
||||
buf.rotate_to_zero();
|
||||
assert_eq!(buf.tail, 0);
|
||||
assert_eq!(buf.head, 3);
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 3);
|
||||
assert_eq!(slices[0][0], 1);
|
||||
assert_eq!(slices[0][1], 2);
|
||||
assert_eq!(slices[0][2], 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_rotate_to_zero_full_no_wrap() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill the buffer
|
||||
let _ = buf.get_ptr_slice(0, 3);
|
||||
|
||||
// Delete all
|
||||
buf.delete_oldest(3);
|
||||
|
||||
// Refill to force a wrap
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 4);
|
||||
slices[0][0] = 1;
|
||||
slices[1][0] = 2;
|
||||
slices[1][1] = 3;
|
||||
slices[1][2] = 4;
|
||||
}
|
||||
assert!(buf.full);
|
||||
|
||||
// Rotate to zero
|
||||
buf.rotate_to_zero();
|
||||
assert!(buf.full);
|
||||
assert_eq!(buf.tail, 0);
|
||||
assert_eq!(buf.head, 0);
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 4);
|
||||
assert_eq!(slices[0], [1, 2, 3, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_resize_grow_from_zero() {
|
||||
let mut buf = CircBuf::new(0, 0u8);
|
||||
assert!(buf.full);
|
||||
|
||||
// Resize
|
||||
buf.resize(2);
|
||||
assert!(!buf.full);
|
||||
assert_eq!(buf.len(), 0);
|
||||
assert_eq!(buf.capacity(), 2);
|
||||
|
||||
buf.append(1).unwrap();
|
||||
buf.append(2).unwrap();
|
||||
|
||||
let slices = buf.get_ptr_slice(0, 2);
|
||||
assert_eq!(slices[0][0], 1);
|
||||
assert_eq!(slices[0][1], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_resize_grow() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill and write
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 4);
|
||||
slices[0].copy_from_slice(&[1, 2, 3, 4]);
|
||||
}
|
||||
assert!(buf.full);
|
||||
|
||||
// Resize
|
||||
buf.resize(6);
|
||||
assert!(!buf.full);
|
||||
assert_eq!(buf.len(), 4);
|
||||
assert_eq!(buf.capacity(), 6);
|
||||
|
||||
let slices = buf.get_ptr_slice(0, 4);
|
||||
assert_eq!(slices[0], [1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_resize_shrink() {
|
||||
let mut buf = CircBuf::new(4, 0u8);
|
||||
|
||||
// Fill and write
|
||||
{
|
||||
let slices = buf.get_ptr_slice(0, 4);
|
||||
slices[0].copy_from_slice(&[1, 2, 3, 4]);
|
||||
}
|
||||
assert!(buf.full);
|
||||
|
||||
// Resize
|
||||
buf.resize(3);
|
||||
assert!(buf.full);
|
||||
assert_eq!(buf.len(), 3);
|
||||
assert_eq!(buf.capacity(), 3);
|
||||
|
||||
let slices = buf.get_ptr_slice(0, 3);
|
||||
assert_eq!(slices[0], [1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_append_empty_slice() {
|
||||
let mut buf = CircBuf::new(5, 0u8);
|
||||
|
||||
// Appending an empty slice to empty buffer should be a no-op
|
||||
buf.append_slice_assume_capacity(b"");
|
||||
assert_eq!(buf.len(), 0);
|
||||
assert!(!buf.full);
|
||||
|
||||
// Buffer should still work normally after appending empty slice
|
||||
buf.append_slice_assume_capacity(b"hi");
|
||||
assert_eq!(buf.len(), 2);
|
||||
|
||||
// Appending an empty slice to non-empty buffer should also be a no-op
|
||||
buf.append_slice_assume_capacity(b"");
|
||||
assert_eq!(buf.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_get_ptr_slice_zero_length() {
|
||||
let mut buf = CircBuf::new(5, 0u8);
|
||||
|
||||
// zero length on empty buffer should return empty slices
|
||||
let slices = buf.get_ptr_slice(0, 0);
|
||||
assert_eq!(slices[0].len(), 0);
|
||||
assert_eq!(slices[1].len(), 0);
|
||||
assert_eq!(buf.len(), 0);
|
||||
|
||||
// Fill buffer partially
|
||||
buf.append_slice_assume_capacity(b"abc");
|
||||
assert_eq!(buf.len(), 3);
|
||||
|
||||
// zero length on non-empty buffer should also work
|
||||
let slices2 = buf.get_ptr_slice(0, 0);
|
||||
assert_eq!(slices2[0].len(), 0);
|
||||
assert_eq!(slices2[1].len(), 0);
|
||||
assert_eq!(buf.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_delete_oldest_zero() {
|
||||
let mut buf = CircBuf::new(5, 0u8);
|
||||
|
||||
// delete_oldest(0) on empty buffer should be a no-op
|
||||
buf.delete_oldest(0);
|
||||
assert_eq!(buf.len(), 0);
|
||||
|
||||
// Fill buffer
|
||||
buf.append_slice_assume_capacity(b"hello");
|
||||
assert_eq!(buf.len(), 5);
|
||||
|
||||
// delete_oldest(0) on non-empty buffer should be a no-op
|
||||
buf.delete_oldest(0);
|
||||
assert_eq!(buf.len(), 5);
|
||||
|
||||
// Verify data is unchanged
|
||||
assert_eq!(buf.iterator(Direction::Forward).next(), Some(&b'h'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circ_buf_ensure_unused_capacity() {
|
||||
let mut buf = CircBuf::new(2, 0u8);
|
||||
buf.append(1).unwrap();
|
||||
buf.ensure_unused_capacity(1);
|
||||
assert_eq!(buf.capacity(), 2);
|
||||
buf.ensure_unused_capacity(5);
|
||||
assert_eq!(buf.capacity(), 6);
|
||||
assert_eq!(collect(&buf, Direction::Forward), [1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
//! Deep/approximate equality helpers (`comparison.zig`).
|
||||
//!
|
||||
//! The Zig version uses comptime reflection to compare arbitrary types
|
||||
//! field-by-field. In Rust, `PartialEq` already covers structural equality;
|
||||
//! [`DeepEqual`] exists to allow a type to customise its "deep equal"
|
||||
//! behaviour independent of `PartialEq` (the Zig `equal` decl), and
|
||||
//! [`ApproxEq`] provides the float-tolerant comparison used by
|
||||
//! `expectApproxEqual`.
|
||||
|
||||
/// A deep equality comparison that works for most types. Types may implement
|
||||
/// this to customise comparison (the Zig `equal` declaration). Blanket
|
||||
/// implementations exist for primitives, strings, and containers of
|
||||
/// `DeepEqual` types.
|
||||
pub trait DeepEqual {
|
||||
fn deep_equal(&self, other: &Self) -> bool;
|
||||
}
|
||||
|
||||
/// Compare two values with [`DeepEqual`].
|
||||
pub fn deep_equal<T: DeepEqual + ?Sized>(old: &T, new: &T) -> bool {
|
||||
old.deep_equal(new)
|
||||
}
|
||||
|
||||
macro_rules! impl_deep_equal_prim {
|
||||
($($t:ty),* $(,)?) => {
|
||||
$(impl DeepEqual for $t {
|
||||
#[allow(clippy::float_cmp)]
|
||||
fn deep_equal(&self, other: &Self) -> bool { self == other }
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
impl_deep_equal_prim!(
|
||||
(),
|
||||
bool,
|
||||
char,
|
||||
i8,
|
||||
i16,
|
||||
i32,
|
||||
i64,
|
||||
i128,
|
||||
isize,
|
||||
u8,
|
||||
u16,
|
||||
u32,
|
||||
u64,
|
||||
u128,
|
||||
usize,
|
||||
f32,
|
||||
f64,
|
||||
str,
|
||||
String,
|
||||
);
|
||||
|
||||
impl<T: DeepEqual> DeepEqual for Option<T> {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => a.deep_equal(b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeepEqual> DeepEqual for [T] {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
self.len() == other.len() && self.iter().zip(other).all(|(a, b)| a.deep_equal(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeepEqual, const N: usize> DeepEqual for [T; N] {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
self.as_slice().deep_equal(other.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeepEqual> DeepEqual for Vec<T> {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
self.as_slice().deep_equal(other.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeepEqual + ?Sized> DeepEqual for &T {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
(**self).deep_equal(*other)
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate equality: relative tolerance of `sqrt(eps)` for floats and
|
||||
/// plain equality for everything else.
|
||||
pub trait ApproxEq {
|
||||
fn approx_eq(&self, other: &Self) -> bool;
|
||||
}
|
||||
|
||||
macro_rules! impl_approx_float {
|
||||
($($t:ty),*) => {
|
||||
$(impl ApproxEq for $t {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
// std.math.approxEqRel with tolerance sqrt(eps)
|
||||
let tolerance = <$t>::EPSILON.sqrt();
|
||||
let (a, b) = (*self, *other);
|
||||
if a == b { return true; }
|
||||
if a.is_nan() || b.is_nan() { return false; }
|
||||
if a.is_infinite() || b.is_infinite() { return false; }
|
||||
(a - b).abs() <= a.abs().max(b.abs()) * tolerance
|
||||
}
|
||||
})*
|
||||
};
|
||||
}
|
||||
impl_approx_float!(f32, f64);
|
||||
|
||||
macro_rules! impl_approx_eq_prim {
|
||||
($($t:ty),* $(,)?) => {
|
||||
$(impl ApproxEq for $t {
|
||||
fn approx_eq(&self, other: &Self) -> bool { self == other }
|
||||
})*
|
||||
};
|
||||
}
|
||||
impl_approx_eq_prim!(
|
||||
(),
|
||||
bool,
|
||||
char,
|
||||
i8,
|
||||
i16,
|
||||
i32,
|
||||
i64,
|
||||
isize,
|
||||
u8,
|
||||
u16,
|
||||
u32,
|
||||
u64,
|
||||
usize,
|
||||
str,
|
||||
String
|
||||
);
|
||||
|
||||
impl<T: ApproxEq> ApproxEq for Option<T> {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => a.approx_eq(b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ApproxEq> ApproxEq for [T] {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
self.len() == other.len() && self.iter().zip(other).all(|(a, b)| a.approx_eq(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ApproxEq, const N: usize> ApproxEq for [T; N] {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
self.as_slice().approx_eq(other.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ApproxEq> ApproxEq for Vec<T> {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
self.as_slice().approx_eq(other.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_approx_tuple {
|
||||
($(($($n:ident : $i:tt),+)),+ $(,)?) => {
|
||||
$(impl<$($n: ApproxEq),+> ApproxEq for ($($n,)+) {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
$(self.$i.approx_eq(&other.$i))&&+
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
impl_approx_tuple!((A: 0), (A: 0, B: 1), (A: 0, B: 1, C: 2), (A: 0, B: 1, C: 2, D: 3));
|
||||
|
||||
/// Asserts that two values are (approximately) equal, panicking otherwise.
|
||||
/// Counterpart of `expectApproxEqual`.
|
||||
#[track_caller]
|
||||
pub fn expect_approx_equal<T: ApproxEq + std::fmt::Debug + ?Sized>(expected: &T, actual: &T) {
|
||||
assert!(
|
||||
expected.approx_eq(actual),
|
||||
"expected approximately {expected:?}, found {actual:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Value {
|
||||
Int(i32),
|
||||
Float(f32),
|
||||
}
|
||||
|
||||
impl ApproxEq for Value {
|
||||
fn approx_eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Value::Int(a), Value::Int(b)) => a.approx_eq(b),
|
||||
(Value::Float(a), Value::Float(b)) => a.approx_eq(b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expect_approx_equal_union_enum() {
|
||||
let b10 = Value::Float(10.0);
|
||||
let b10plus = Value::Float(10.000001);
|
||||
expect_approx_equal(&b10, &b10plus);
|
||||
assert!(!Value::Int(1).approx_eq(&Value::Float(1.0)));
|
||||
assert!(!Value::Float(10.0).approx_eq(&Value::Float(10.1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expect_approx_equal_nested_array() {
|
||||
let a = [[1.0f32, 0.0], [0.0, 1.0]];
|
||||
let b = [[1.000001f32, 0.0], [0.0, 0.999999]];
|
||||
expect_approx_equal(&a, &b);
|
||||
let c = [[1.1f32, 0.0], [0.0, 1.0]];
|
||||
assert!(!a.approx_eq(&c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expect_approx_equal_vector() {
|
||||
// Zig @Vector(4, f32) maps to a fixed array.
|
||||
let a = [4.0f32; 4];
|
||||
let b = [4.000001f32; 4];
|
||||
expect_approx_equal(&a, &b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expect_approx_equal_struct() {
|
||||
// Zig anonymous struct maps to a tuple.
|
||||
let a = (1, 1.0f32);
|
||||
let b = (1, 0.999999f32);
|
||||
expect_approx_equal(&a, &b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_void() {
|
||||
assert!(deep_equal(&(), &()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_bool() {
|
||||
assert!(deep_equal(&true, &true));
|
||||
assert!(deep_equal(&false, &false));
|
||||
assert!(!deep_equal(&true, &false));
|
||||
assert!(!deep_equal(&false, &true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_int() {
|
||||
assert!(deep_equal(&42i32, &42));
|
||||
assert!(deep_equal(&-100i32, &-100));
|
||||
assert!(!deep_equal(&42i32, &43));
|
||||
assert!(deep_equal(&0u64, &0));
|
||||
assert!(!deep_equal(&0u64, &1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_float() {
|
||||
assert!(deep_equal(&1.0f32, &1.0));
|
||||
assert!(!deep_equal(&1.0f32, &1.1));
|
||||
assert!(deep_equal(&1.23456f64, &1.23456));
|
||||
assert!(!deep_equal(&1.23456f64, &1.23455));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_enum() {
|
||||
#[derive(PartialEq)]
|
||||
enum Color {
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
}
|
||||
impl DeepEqual for Color {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
self == other
|
||||
}
|
||||
}
|
||||
assert!(deep_equal(&Color::Red, &Color::Red));
|
||||
assert!(deep_equal(&Color::Blue, &Color::Blue));
|
||||
assert!(!deep_equal(&Color::Red, &Color::Green));
|
||||
assert!(!deep_equal(&Color::Green, &Color::Blue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_slice_u8() {
|
||||
assert!(deep_equal("hello", "hello"));
|
||||
assert!(deep_equal("", ""));
|
||||
assert!(!deep_equal("hello", "world"));
|
||||
assert!(!deep_equal("hello", "hell"));
|
||||
assert!(!deep_equal("hello", "hello!"));
|
||||
assert!(deep_equal(b"hello".as_slice(), b"hello".as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_sentinel_slice_u8() {
|
||||
// [:0]const u8 maps to &str / CString-like; plain strings here.
|
||||
assert!(deep_equal("foo", "foo"));
|
||||
assert!(!deep_equal("foo", "bar"));
|
||||
assert!(!deep_equal("foo", "fo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_slice_of_strings() {
|
||||
let a: &[&str] = &["one", "two", "three"];
|
||||
let b: &[&str] = &["one", "two", "three"];
|
||||
let c: &[&str] = &["one", "two"];
|
||||
let d: &[&str] = &["one", "two", "four"];
|
||||
let e: &[&str] = &[];
|
||||
|
||||
assert!(deep_equal(a, b));
|
||||
assert!(!deep_equal(a, c));
|
||||
assert!(!deep_equal(a, d));
|
||||
assert!(deep_equal(e, e));
|
||||
assert!(!deep_equal(a, e));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_optional() {
|
||||
assert!(deep_equal::<Option<i32>>(&None, &None));
|
||||
assert!(deep_equal(&Some(42), &Some(42)));
|
||||
assert!(!deep_equal(&None, &Some(42)));
|
||||
assert!(!deep_equal(&Some(42), &None));
|
||||
assert!(!deep_equal(&Some(42), &Some(43)));
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Nested {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
impl DeepEqual for Nested {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
self.x.deep_equal(&other.x) && self.y.deep_equal(&other.y)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_optional_nested() {
|
||||
assert!(deep_equal::<Option<Nested>>(&None, &None));
|
||||
assert!(deep_equal(
|
||||
&Some(Nested { x: 1, y: 2 }),
|
||||
&Some(Nested { x: 1, y: 2 })
|
||||
));
|
||||
assert!(!deep_equal(
|
||||
&Some(Nested { x: 1, y: 2 }),
|
||||
&Some(Nested { x: 1, y: 3 })
|
||||
));
|
||||
assert!(!deep_equal(&Some(Nested { x: 1, y: 2 }), &None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_array() {
|
||||
assert!(deep_equal(&[1, 2, 3], &[1, 2, 3]));
|
||||
assert!(!deep_equal(&[1, 2, 3], &[1, 2, 4]));
|
||||
assert!(!deep_equal(&[1, 2, 3], &[0, 2, 3]));
|
||||
assert!(deep_equal::<[i32; 0]>(&[], &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_nested_array() {
|
||||
let a = [[1, 2], [3, 4]];
|
||||
let b = [[1, 2], [3, 4]];
|
||||
let c = [[1, 2], [3, 5]];
|
||||
assert!(deep_equal(&a, &b));
|
||||
assert!(!deep_equal(&a, &c));
|
||||
}
|
||||
|
||||
struct Outer {
|
||||
a: Nested,
|
||||
b: Nested,
|
||||
}
|
||||
impl DeepEqual for Outer {
|
||||
fn deep_equal(&self, o: &Self) -> bool {
|
||||
self.a.deep_equal(&o.a) && self.b.deep_equal(&o.b)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_struct() {
|
||||
assert!(deep_equal(
|
||||
&Nested { x: 10, y: 20 },
|
||||
&Nested { x: 10, y: 20 }
|
||||
));
|
||||
assert!(!deep_equal(
|
||||
&Nested { x: 10, y: 20 },
|
||||
&Nested { x: 10, y: 21 }
|
||||
));
|
||||
assert!(!deep_equal(
|
||||
&Nested { x: 10, y: 20 },
|
||||
&Nested { x: 11, y: 20 }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_struct_nested() {
|
||||
let mk = |a, b| Outer {
|
||||
a: Nested { x: a, y: 0 },
|
||||
b: Nested { x: b, y: 0 },
|
||||
};
|
||||
assert!(deep_equal(&mk(1, 2), &mk(1, 2)));
|
||||
assert!(!deep_equal(&mk(1, 2), &mk(1, 3)));
|
||||
}
|
||||
|
||||
struct Custom {
|
||||
value: i32,
|
||||
}
|
||||
impl DeepEqual for Custom {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
self.value.rem_euclid(10) == other.value.rem_euclid(10)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_struct_with_equal_decl() {
|
||||
assert!(deep_equal(&Custom { value: 5 }, &Custom { value: 15 }));
|
||||
assert!(deep_equal(&Custom { value: 100 }, &Custom { value: 0 }));
|
||||
assert!(!deep_equal(&Custom { value: 5 }, &Custom { value: 6 }));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum U {
|
||||
Int(i32),
|
||||
Float(f32),
|
||||
None,
|
||||
}
|
||||
impl DeepEqual for U {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(U::Int(a), U::Int(b)) => a == b,
|
||||
(U::Float(a), U::Float(b)) => a == b,
|
||||
(U::None, U::None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_union() {
|
||||
assert!(deep_equal(&U::Int(42), &U::Int(42)));
|
||||
assert!(!deep_equal(&U::Int(42), &U::Int(43)));
|
||||
assert!(!deep_equal(&U::Int(42), &U::Float(42.0)));
|
||||
assert!(deep_equal(&U::None, &U::None));
|
||||
assert!(!deep_equal(&U::None, &U::Int(0)));
|
||||
}
|
||||
|
||||
enum UCustom {
|
||||
Num(i32),
|
||||
Str(&'static str),
|
||||
}
|
||||
impl DeepEqual for UCustom {
|
||||
fn deep_equal(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(UCustom::Num(n), UCustom::Num(m)) => n.rem_euclid(10) == m.rem_euclid(10),
|
||||
(UCustom::Str(s), UCustom::Str(t)) => s.len() == t.len(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_union_with_equal_decl() {
|
||||
assert!(deep_equal(&UCustom::Num(5), &UCustom::Num(25)));
|
||||
assert!(!deep_equal(&UCustom::Num(5), &UCustom::Num(6)));
|
||||
assert!(deep_equal(&UCustom::Str("abc"), &UCustom::Str("xyz")));
|
||||
assert!(!deep_equal(&UCustom::Str("abc"), &UCustom::Str("ab")));
|
||||
}
|
||||
|
||||
struct Item {
|
||||
id: i32,
|
||||
name: &'static str,
|
||||
}
|
||||
impl DeepEqual for Item {
|
||||
fn deep_equal(&self, o: &Self) -> bool {
|
||||
self.id.deep_equal(&o.id) && self.name.deep_equal(o.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_array_of_structs() {
|
||||
let mk = |n: &'static str| [Item { id: 1, name: "one" }, Item { id: 2, name: n }];
|
||||
assert!(deep_equal(&mk("two"), &mk("two")));
|
||||
assert!(!deep_equal(&mk("two"), &mk("TWO")));
|
||||
}
|
||||
|
||||
struct Config {
|
||||
name: &'static str,
|
||||
port: Option<u16>,
|
||||
}
|
||||
impl DeepEqual for Config {
|
||||
fn deep_equal(&self, o: &Self) -> bool {
|
||||
self.name.deep_equal(o.name) && self.port.deep_equal(&o.port)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_struct_with_optional_field() {
|
||||
let c = |port| Config { name: "app", port };
|
||||
assert!(deep_equal(&c(Some(8080)), &c(Some(8080))));
|
||||
assert!(deep_equal(&c(None), &c(None)));
|
||||
assert!(!deep_equal(&c(Some(8080)), &c(None)));
|
||||
assert!(!deep_equal(&c(Some(8080)), &c(Some(8081))));
|
||||
}
|
||||
|
||||
struct Data {
|
||||
values: [i32; 3],
|
||||
}
|
||||
impl DeepEqual for Data {
|
||||
fn deep_equal(&self, o: &Self) -> bool {
|
||||
self.values.deep_equal(&o.values)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_struct_with_array_field() {
|
||||
assert!(deep_equal(
|
||||
&Data { values: [1, 2, 3] },
|
||||
&Data { values: [1, 2, 3] }
|
||||
));
|
||||
assert!(!deep_equal(
|
||||
&Data { values: [1, 2, 3] },
|
||||
&Data { values: [1, 2, 4] }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_equal_vec() {
|
||||
assert!(deep_equal(&vec![1, 2], &vec![1, 2]));
|
||||
assert!(!deep_equal(&vec![1, 2], &vec![1]));
|
||||
}
|
||||
}
|
||||
@@ -1 +1,40 @@
|
||||
//! ghostty-datastruct: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
//! The datastruct package contains data structures or anything closely
|
||||
//! related to data structures. Port of `src/datastruct/*`.
|
||||
//!
|
||||
//! `intrusive_linked_list.zig` is intentionally not ported: its only user
|
||||
//! (`lru.zig`) is implemented with an index-based list here. See
|
||||
//! `docs/rewrite/notes/ghostty-datastruct.md`.
|
||||
|
||||
#![cfg_attr(
|
||||
test,
|
||||
allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::float_cmp,
|
||||
clippy::assert_is_empty,
|
||||
clippy::redundant_closure_for_method_calls
|
||||
)
|
||||
)]
|
||||
|
||||
pub mod array_list_collection;
|
||||
pub mod blocking_queue;
|
||||
pub mod cache_table;
|
||||
pub mod circ_buf;
|
||||
pub mod comparison;
|
||||
pub mod lru;
|
||||
pub mod message_data;
|
||||
pub mod segmented_pool;
|
||||
pub mod split_tree;
|
||||
|
||||
pub use array_list_collection::ArrayListCollection;
|
||||
pub use blocking_queue::BlockingQueue;
|
||||
pub use cache_table::CacheTable;
|
||||
pub use circ_buf::CircBuf;
|
||||
pub use message_data::MessageData;
|
||||
pub use segmented_pool::SegmentedPool;
|
||||
pub use split_tree::SplitTree;
|
||||
|
||||
/// Stand-in for Zig's `Allocator.Error.OutOfMemory`, returned where a fixed
|
||||
/// capacity structure cannot accept more data.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("out of memory")]
|
||||
pub struct OutOfMemory;
|
||||
@@ -0,0 +1,389 @@
|
||||
//! A hash map with least-recently-used eviction (`lru.zig`).
|
||||
//!
|
||||
//! The Zig version threads an intrusive `std.DoublyLinkedList` through
|
||||
//! allocated entries; here the entries live in a slab and are linked by
|
||||
//! index.
|
||||
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
use std::hash::{BuildHasher, Hash, RandomState};
|
||||
|
||||
/// Create a HashMap for a key type that can be automatically hashed.
|
||||
/// If you want finer-grained control, use [`HashMap`] directly.
|
||||
pub type AutoHashMap<K, V> = HashMap<K, V, RandomState>;
|
||||
|
||||
/// A key/value pair.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Kv<K, V> {
|
||||
pub key: K,
|
||||
pub value: V,
|
||||
}
|
||||
|
||||
/// The result of a `get_or_put` operation.
|
||||
#[derive(Debug)]
|
||||
pub struct GetOrPutResult<'a, K, V> {
|
||||
/// The entry that was retrieved. If `found_existing` is false,
|
||||
/// then this is a reference to space to store a V (initialized to
|
||||
/// `V::default()`). If `found_existing` is true, the value is valid, but
|
||||
/// can be overwritten.
|
||||
pub value_ptr: &'a mut V,
|
||||
|
||||
/// Whether an existing value was found or not.
|
||||
pub found_existing: bool,
|
||||
|
||||
/// If another entry had to be evicted to make space for this
|
||||
/// put operation, then this is the value that was evicted.
|
||||
pub evicted: Option<Kv<K, V>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Entry<K, V> {
|
||||
key: K,
|
||||
value: V,
|
||||
prev: Option<usize>,
|
||||
next: Option<usize>,
|
||||
}
|
||||
|
||||
/// HashMap implementation that supports least-recently-used eviction.
|
||||
///
|
||||
/// Note: This is a really elementary CS101 version of an LRU right now.
|
||||
/// This is done initially to get something working. Once we have it working,
|
||||
/// we can benchmark and improve if this ends up being a source of slowness.
|
||||
#[derive(Debug)]
|
||||
pub struct HashMap<K, V, S = RandomState> {
|
||||
/// Map to maintain our entries (key -> slab index).
|
||||
map: StdHashMap<K, usize, S>,
|
||||
|
||||
/// Slab of entries.
|
||||
entries: Vec<Option<Entry<K, V>>>,
|
||||
free: Vec<usize>,
|
||||
|
||||
/// Queue to maintain LRU order: `first` is the least recently used.
|
||||
first: Option<usize>,
|
||||
last: Option<usize>,
|
||||
|
||||
/// The capacity of our map. If this capacity is reached, cache
|
||||
/// misses will begin evicting entries.
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl<K: Hash + Eq + Clone, V: Default> HashMap<K, V, RandomState> {
|
||||
#[must_use]
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self::with_hasher(capacity, RandomState::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Hash + Eq + Clone, V: Default, S: BuildHasher> HashMap<K, V, S> {
|
||||
#[must_use]
|
||||
pub fn with_hasher(capacity: usize, hasher: S) -> Self {
|
||||
Self {
|
||||
map: StdHashMap::with_hasher(hasher),
|
||||
entries: Vec::new(),
|
||||
free: Vec::new(),
|
||||
first: None,
|
||||
last: None,
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// The capacity of the map.
|
||||
#[must_use]
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
/// Number of entries.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
|
||||
fn entry(&self, idx: usize) -> &Entry<K, V> {
|
||||
self.entries[idx].as_ref().expect("live lru entry")
|
||||
}
|
||||
|
||||
fn entry_mut(&mut self, idx: usize) -> &mut Entry<K, V> {
|
||||
self.entries[idx].as_mut().expect("live lru entry")
|
||||
}
|
||||
|
||||
/// Unlink an entry from the queue.
|
||||
fn unlink(&mut self, idx: usize) {
|
||||
let (prev, next) = {
|
||||
let e = self.entry(idx);
|
||||
(e.prev, e.next)
|
||||
};
|
||||
match prev {
|
||||
Some(p) => self.entry_mut(p).next = next,
|
||||
None => self.first = next,
|
||||
}
|
||||
match next {
|
||||
Some(n) => self.entry_mut(n).prev = prev,
|
||||
None => self.last = prev,
|
||||
}
|
||||
let e = self.entry_mut(idx);
|
||||
e.prev = None;
|
||||
e.next = None;
|
||||
}
|
||||
|
||||
/// Append an (unlinked) entry to the end (most recently used).
|
||||
fn append(&mut self, idx: usize) {
|
||||
let last = self.last;
|
||||
{
|
||||
let e = self.entry_mut(idx);
|
||||
e.prev = last;
|
||||
e.next = None;
|
||||
}
|
||||
match last {
|
||||
Some(l) => self.entry_mut(l).next = Some(idx),
|
||||
None => self.first = Some(idx),
|
||||
}
|
||||
self.last = Some(idx);
|
||||
}
|
||||
|
||||
/// Get or put a value for a key. See [`GetOrPutResult`] on how to check
|
||||
/// if an existing value was found, if an existing value was evicted,
|
||||
/// etc.
|
||||
pub fn get_or_put(&mut self, key: K) -> GetOrPutResult<'_, K, V> {
|
||||
if let Some(&idx) = self.map.get(&key) {
|
||||
// Move to end to mark as most recently used
|
||||
self.unlink(idx);
|
||||
self.append(idx);
|
||||
|
||||
return GetOrPutResult {
|
||||
found_existing: true,
|
||||
value_ptr: &mut self.entry_mut(idx).value,
|
||||
evicted: None,
|
||||
};
|
||||
}
|
||||
|
||||
// We're evicting if our map insertion would increase our size
|
||||
// beyond capacity.
|
||||
let evict = self.map.len() + 1 > self.capacity;
|
||||
|
||||
let (idx, evicted) = if evict {
|
||||
// Our first node is the least recently used. We avoid
|
||||
// allocation by reusing the entry we evict.
|
||||
let lru = self.first.expect("capacity must be non-zero to evict");
|
||||
self.unlink(lru);
|
||||
|
||||
// Remove the least used from the map
|
||||
let old_key = self.entry(lru).key.clone();
|
||||
self.map.remove(&old_key);
|
||||
|
||||
let e = self.entry_mut(lru);
|
||||
let old_key = std::mem::replace(&mut e.key, key.clone());
|
||||
let old_value = std::mem::take(&mut e.value);
|
||||
(
|
||||
lru,
|
||||
Some(Kv {
|
||||
key: old_key,
|
||||
value: old_value,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
let entry = Entry {
|
||||
key: key.clone(),
|
||||
value: V::default(),
|
||||
prev: None,
|
||||
next: None,
|
||||
};
|
||||
let idx = if let Some(i) = self.free.pop() {
|
||||
self.entries[i] = Some(entry);
|
||||
i
|
||||
} else {
|
||||
self.entries.push(Some(entry));
|
||||
self.entries.len() - 1
|
||||
};
|
||||
(idx, None)
|
||||
};
|
||||
|
||||
// Store our entry in the map and mark it as most recently used.
|
||||
self.map.insert(key, idx);
|
||||
self.append(idx);
|
||||
|
||||
GetOrPutResult {
|
||||
found_existing: false,
|
||||
value_ptr: &mut self.entry_mut(idx).value,
|
||||
evicted,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a value for a key. This does not change the LRU order.
|
||||
#[must_use]
|
||||
pub fn get(&self, key: &K) -> Option<&V> {
|
||||
let idx = *self.map.get(key)?;
|
||||
Some(&self.entry(idx).value)
|
||||
}
|
||||
|
||||
/// Resize the LRU. If this shrinks the LRU then LRU items will be
|
||||
/// removed. The removed values are returned (oldest first).
|
||||
pub fn resize(&mut self, capacity: usize) -> Option<Vec<V>> {
|
||||
// Fastest
|
||||
if capacity >= self.capacity {
|
||||
self.capacity = capacity;
|
||||
return None;
|
||||
}
|
||||
|
||||
// If we're shrinking but we're smaller than the new capacity,
|
||||
// then we don't have to do anything.
|
||||
if self.map.len() <= capacity {
|
||||
self.capacity = capacity;
|
||||
return None;
|
||||
}
|
||||
|
||||
// We're shrinking and we have more items than the new capacity
|
||||
let delta = self.map.len() - capacity;
|
||||
let mut evicted = Vec::with_capacity(delta);
|
||||
for _ in 0..delta {
|
||||
let idx = self.first.expect("non-empty queue");
|
||||
self.unlink(idx);
|
||||
let entry = self.entries[idx].take().expect("live lru entry");
|
||||
self.free.push(idx);
|
||||
self.map.remove(&entry.key);
|
||||
evicted.push(entry.value);
|
||||
}
|
||||
|
||||
self.capacity = capacity;
|
||||
assert_eq!(self.map.len(), capacity);
|
||||
|
||||
Some(evicted)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn get_or_put() {
|
||||
let mut m = AutoHashMap::<u32, u8>::new(2);
|
||||
|
||||
// Insert cap values, should be hits
|
||||
{
|
||||
let gop = m.get_or_put(1);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_none());
|
||||
*gop.value_ptr = 1;
|
||||
}
|
||||
{
|
||||
let gop = m.get_or_put(2);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_none());
|
||||
*gop.value_ptr = 2;
|
||||
}
|
||||
|
||||
// 1 is LRU
|
||||
assert!(m.get_or_put(1).found_existing);
|
||||
assert!(m.get_or_put(2).found_existing);
|
||||
|
||||
// Next should evict
|
||||
{
|
||||
let gop = m.get_or_put(3);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_some());
|
||||
assert_eq!(gop.evicted.unwrap().value, 1);
|
||||
*gop.value_ptr = 3;
|
||||
}
|
||||
|
||||
// Currently: 2 is LRU, let's make 3 LRU
|
||||
assert!(m.get_or_put(2).found_existing);
|
||||
|
||||
// Next should evict
|
||||
{
|
||||
let gop = m.get_or_put(4);
|
||||
assert!(!gop.found_existing);
|
||||
let ev = gop.evicted.unwrap();
|
||||
assert_eq!(ev.value, 3);
|
||||
assert_eq!(ev.key, 3);
|
||||
*gop.value_ptr = 4;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get() {
|
||||
let mut m = AutoHashMap::<u32, u8>::new(2);
|
||||
|
||||
// Insert cap values, should be hits
|
||||
{
|
||||
let gop = m.get_or_put(1);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_none());
|
||||
*gop.value_ptr = 1;
|
||||
}
|
||||
|
||||
assert!(m.get(&1).is_some());
|
||||
assert_eq!(*m.get(&1).unwrap(), 1);
|
||||
assert!(m.get(&2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_shrink_without_removal() {
|
||||
let mut m = AutoHashMap::<u32, u8>::new(2);
|
||||
|
||||
// Insert cap values, LRU is 1
|
||||
{
|
||||
let gop = m.get_or_put(1);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_none());
|
||||
*gop.value_ptr = 1;
|
||||
}
|
||||
|
||||
// Shrink
|
||||
let evicted = m.resize(1);
|
||||
assert!(evicted.is_none());
|
||||
{
|
||||
let gop = m.get_or_put(1);
|
||||
assert!(gop.found_existing);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_shrink_and_remove() {
|
||||
let mut m = AutoHashMap::<u32, u8>::new(2);
|
||||
|
||||
// Insert cap values, LRU is 1
|
||||
{
|
||||
let gop = m.get_or_put(1);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_none());
|
||||
*gop.value_ptr = 1;
|
||||
}
|
||||
{
|
||||
let gop = m.get_or_put(2);
|
||||
assert!(!gop.found_existing);
|
||||
assert!(gop.evicted.is_none());
|
||||
*gop.value_ptr = 2;
|
||||
}
|
||||
|
||||
// Shrink
|
||||
let evicted = m.resize(1).unwrap();
|
||||
assert_eq!(evicted.len(), 1);
|
||||
{
|
||||
let gop = m.get_or_put(1);
|
||||
assert!(!gop.found_existing);
|
||||
assert_eq!(gop.evicted.as_ref().unwrap().value, 2);
|
||||
*gop.value_ptr = 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_grow_and_slot_reuse() {
|
||||
let mut m = AutoHashMap::<u32, u8>::new(1);
|
||||
*m.get_or_put(1).value_ptr = 1;
|
||||
assert!(m.resize(3).is_none());
|
||||
*m.get_or_put(2).value_ptr = 2;
|
||||
*m.get_or_put(3).value_ptr = 3;
|
||||
assert_eq!(m.len(), 3);
|
||||
// Shrink to 1 evicts oldest-first and frees slots for reuse.
|
||||
assert_eq!(m.resize(1).unwrap(), vec![1, 2]);
|
||||
*m.get_or_put(4).value_ptr = 4;
|
||||
assert_eq!(m.len(), 1);
|
||||
assert_eq!(m.get(&4), Some(&4));
|
||||
assert_eq!(m.get(&3), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Data that fits in an array, is a stable pointer, or is heap allocated
|
||||
//! (`message_data.zig`). Helpful for thread messaging utilities.
|
||||
|
||||
/// A union that can be used to accommodate data that fit within an array,
|
||||
/// are a stable pointer, or require deallocation. This is helpful for thread
|
||||
/// messaging utilities.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MessageData<Elem: Copy + Default + 'static, const SMALL_SIZE: usize> {
|
||||
/// A small write where the data fits into this union size.
|
||||
Small {
|
||||
data: [Elem; SMALL_SIZE],
|
||||
len: usize,
|
||||
},
|
||||
|
||||
/// A stable pointer so we can just pass the slice directly through.
|
||||
/// This is useful i.e. for const data.
|
||||
Stable(&'static [Elem]),
|
||||
|
||||
/// Allocated. Freed on drop. This should be rarely used.
|
||||
Alloc(Vec<Elem>),
|
||||
}
|
||||
|
||||
impl<Elem: Copy + Default + 'static, const SMALL_SIZE: usize> MessageData<Elem, SMALL_SIZE> {
|
||||
/// The maximum number of elements in the small variant.
|
||||
pub const MAX: usize = SMALL_SIZE;
|
||||
|
||||
/// Initializes the union for a given data type. This will
|
||||
/// attempt to fit into a small value if possible, otherwise
|
||||
/// will allocate and put into `Alloc`.
|
||||
///
|
||||
/// This can't and will never detect stable pointers.
|
||||
#[must_use]
|
||||
pub fn init(data: &[Elem]) -> Self {
|
||||
// If it fits in our small request, do that.
|
||||
if data.len() <= SMALL_SIZE {
|
||||
let mut buf = [Elem::default(); SMALL_SIZE];
|
||||
buf[..data.len()].copy_from_slice(data);
|
||||
return Self::Small {
|
||||
data: buf,
|
||||
len: data.len(),
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, allocate
|
||||
Self::Alloc(data.to_vec())
|
||||
}
|
||||
|
||||
/// Returns a const slice of the data pointed to by this request.
|
||||
#[must_use]
|
||||
pub fn slice(&self) -> &[Elem] {
|
||||
match self {
|
||||
Self::Small { data, len } => &data[..*len],
|
||||
Self::Stable(v) => v,
|
||||
Self::Alloc(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn message_data_init_small() {
|
||||
let io = MessageData::<u8, 10>::init(b"hello!");
|
||||
assert!(matches!(io, MessageData::Small { .. }));
|
||||
assert_eq!(io.slice(), b"hello!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_data_init_alloc() {
|
||||
let input = "hello! ".repeat(100);
|
||||
let io = MessageData::<u8, 10>::init(input.as_bytes());
|
||||
assert!(matches!(io, MessageData::Alloc(_)));
|
||||
assert_eq!(io.slice(), input.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_data_small_fits_non_u8_sized_data() {
|
||||
let input = [b'X'; 500];
|
||||
let io = MessageData::<u8, 500>::init(&input);
|
||||
assert!(matches!(io, MessageData::Small { .. }));
|
||||
assert_eq!(io.slice().len(), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_data_stable() {
|
||||
let io = MessageData::<u8, 4>::Stable(b"static");
|
||||
assert_eq!(io.slice(), b"static");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! A pool of values with stable slots (`segmented_pool.zig`).
|
||||
|
||||
/// Returned by [`SegmentedPool::get`] when there are no values available.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("out of values")]
|
||||
pub struct OutOfValues;
|
||||
|
||||
/// A data structure where you can get stable (never moved) slots for
|
||||
/// a type that automatically grows if necessary. The values can be "put back"
|
||||
/// but are expected to be put back IN ORDER.
|
||||
///
|
||||
/// This is implemented specifically for libuv write requests, since the
|
||||
/// write requests must have a stable pointer and are guaranteed to be processed
|
||||
/// in order for a single stream.
|
||||
///
|
||||
/// Instead of raw pointers, slots are identified by a stable index returned
|
||||
/// from [`get`](Self::get) and accessed with [`at`](Self::at) /
|
||||
/// [`at_mut`](Self::at_mut).
|
||||
///
|
||||
/// This is NOT thread safe.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SegmentedPool<T: Default, const PREALLOC: usize> {
|
||||
i: usize,
|
||||
available: usize,
|
||||
list: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T: Default, const PREALLOC: usize> Default for SegmentedPool<T, PREALLOC> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default, const PREALLOC: usize> SegmentedPool<T, PREALLOC> {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
i: 0,
|
||||
available: PREALLOC,
|
||||
list: (0..PREALLOC).map(|_| T::default()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of values currently available without growing.
|
||||
#[must_use]
|
||||
pub fn available(&self) -> usize {
|
||||
self.available
|
||||
}
|
||||
|
||||
/// Total number of slots.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.list.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.list.is_empty()
|
||||
}
|
||||
|
||||
/// Access the slot at an index returned by `get`.
|
||||
#[must_use]
|
||||
pub fn at(&self, idx: usize) -> &T {
|
||||
&self.list[idx]
|
||||
}
|
||||
|
||||
/// Mutably access the slot at an index returned by `get`.
|
||||
pub fn at_mut(&mut self, idx: usize) -> &mut T {
|
||||
&mut self.list[idx]
|
||||
}
|
||||
|
||||
/// Get the next available value out of the list (as a slot index). This
|
||||
/// will not grow the list.
|
||||
pub fn get(&mut self) -> Result<usize, OutOfValues> {
|
||||
// Error to not have any
|
||||
if self.available == 0 {
|
||||
return Err(OutOfValues);
|
||||
}
|
||||
|
||||
// The index we grab is just i % len, so we wrap around to the front.
|
||||
let i = self.i % self.list.len();
|
||||
self.i = self.i.wrapping_add(1); // Wrapping addition so we go back to 0
|
||||
self.available -= 1;
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
/// Get the next available value out of the list and grow the list
|
||||
/// if necessary.
|
||||
pub fn get_grow(&mut self) -> Result<usize, OutOfValues> {
|
||||
if self.available == 0 {
|
||||
self.grow();
|
||||
}
|
||||
self.get()
|
||||
}
|
||||
|
||||
fn grow(&mut self) {
|
||||
let old = self.list.len();
|
||||
self.list.extend((0..old).map(|_| T::default()));
|
||||
self.i = old;
|
||||
self.available = old;
|
||||
}
|
||||
|
||||
/// Put a value back. The value put back is expected to be the
|
||||
/// in order of get.
|
||||
pub fn put(&mut self) {
|
||||
self.available += 1;
|
||||
assert!(self.available <= self.list.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn segmented_pool() {
|
||||
let mut list: SegmentedPool<u8, 2> = SegmentedPool::new();
|
||||
assert_eq!(list.available(), 2);
|
||||
|
||||
// Get to capacity
|
||||
let v1 = list.get().unwrap();
|
||||
let v2 = list.get().unwrap();
|
||||
assert_ne!(v1, v2);
|
||||
assert_eq!(list.get(), Err(OutOfValues));
|
||||
|
||||
// Test writing for later
|
||||
*list.at_mut(v1) = 42;
|
||||
|
||||
// Put a value back
|
||||
list.put();
|
||||
let temp = list.get().unwrap();
|
||||
assert_eq!(v1, temp);
|
||||
assert_eq!(*list.at(temp), 42);
|
||||
assert_eq!(list.get(), Err(OutOfValues));
|
||||
|
||||
// Grow
|
||||
let v3 = list.get_grow().unwrap();
|
||||
assert!(v1 != v3 && v2 != v3);
|
||||
list.get().unwrap();
|
||||
assert_eq!(list.get(), Err(OutOfValues));
|
||||
|
||||
// Put a value back
|
||||
list.put();
|
||||
assert_eq!(v1, list.get().unwrap());
|
||||
assert_eq!(list.get(), Err(OutOfValues));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1706 @@
|
||||
//! `SplitTree` represents a tree of view types that can be divided.
|
||||
//!
|
||||
//! Concretely for Ghostty, it represents a tree of terminal views. In
|
||||
//! its basic state, there are no splits and it is a single full-sized
|
||||
//! terminal. However, it can be split arbitrarily many times among two
|
||||
//! axes (horizontal and vertical) to create a tree of terminal views.
|
||||
//!
|
||||
//! This is an immutable tree structure, meaning all operations on it
|
||||
//! will return a new tree with the operation applied. This allows us to
|
||||
//! store versions of the tree in a history for easy undo/redo. To facilitate
|
||||
//! this, the stored View type must implement reference counting; in Rust that
|
||||
//! is expressed with [`Clone`] (the Zig `ref`; e.g. `Rc<T>`, a GObject
|
||||
//! handle) and [`Drop`] (the Zig `unref`).
|
||||
//!
|
||||
//! Optionally a view can implement [`SplitTreeView::split_tree_label`] to
|
||||
//! provide a label used by the debug view. If this isn't specified then the
|
||||
//! node handle will be used.
|
||||
//!
|
||||
//! Deviations from the Zig version: allocation is infallible (no allocator
|
||||
//! parameters / `Allocator.Error`), the arena is replaced by a `Vec`, and the
|
||||
//! GObject boxed-type glue (`getGObjectType`) is left to the GTK crate.
|
||||
|
||||
use half::f16;
|
||||
use std::fmt;
|
||||
|
||||
/// The requirements on the view type stored in a [`SplitTree`].
|
||||
pub trait SplitTreeView: Clone {
|
||||
/// Return a label that is used for the debug view. If this returns
|
||||
/// `None` then the node handle will be used.
|
||||
fn split_tree_label(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by [`SplitTree::split`] when the resulting tree would have
|
||||
/// more nodes than a [`Handle`] can address (`error.OutOfMemory` in Zig).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum SplitError {
|
||||
#[error("out of memory: too many nodes in split tree")]
|
||||
OutOfMemory,
|
||||
}
|
||||
|
||||
/// A handle into the nodes array. This lets us keep track of nodes with
|
||||
/// 16-bit handles rather than full pointer-width values.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Handle(u16);
|
||||
|
||||
impl Handle {
|
||||
/// The root node (index 0).
|
||||
pub const ROOT: Handle = Handle(0);
|
||||
|
||||
/// Create a handle from an index.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the index doesn't fit in the handle backing type.
|
||||
#[must_use]
|
||||
pub fn from_idx(idx: usize) -> Handle {
|
||||
Handle(u16::try_from(idx).expect("split tree handle out of range"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn idx(self) -> usize {
|
||||
usize::from(self.0)
|
||||
}
|
||||
|
||||
/// Offset the handle by a given amount.
|
||||
#[must_use]
|
||||
pub fn offset(self, v: usize) -> Handle {
|
||||
let final_ = self.idx() + v;
|
||||
assert!(final_ < usize::from(u16::MAX));
|
||||
Handle::from_idx(final_)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Node<V> {
|
||||
Leaf(V),
|
||||
Split(Split),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
Down,
|
||||
Up,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Split {
|
||||
pub layout: Layout,
|
||||
pub ratio: f16,
|
||||
pub left: Handle,
|
||||
pub right: Handle,
|
||||
}
|
||||
|
||||
/// Alias matching the Zig `Split.Layout` / `Split.Direction` names.
|
||||
pub type SplitLayout = Layout;
|
||||
pub type SplitDirection = Direction;
|
||||
|
||||
/// A view in the tree with its handle.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ViewEntry<'a, V> {
|
||||
pub handle: Handle,
|
||||
pub view: &'a V,
|
||||
}
|
||||
|
||||
/// An iterator over all the views in the tree.
|
||||
pub struct Iter<'a, V> {
|
||||
i: usize,
|
||||
nodes: &'a [Node<V>],
|
||||
}
|
||||
|
||||
impl<'a, V> Iterator for Iter<'a, V> {
|
||||
type Item = ViewEntry<'a, V>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
// If we have no nodes, return None.
|
||||
if self.i >= self.nodes.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the current node and increment the index.
|
||||
let handle = Handle::from_idx(self.i);
|
||||
self.i += 1;
|
||||
if let Node::Leaf(v) = &self.nodes[handle.idx()] {
|
||||
return Some(ViewEntry { handle, view: v });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Goto {
|
||||
/// Previous view, null if we're the first view.
|
||||
Previous,
|
||||
|
||||
/// Next view, null if we're the last view.
|
||||
Next,
|
||||
|
||||
/// Previous view, but wrapped around to the last view. May
|
||||
/// return the same view if this is the first view.
|
||||
PreviousWrapped,
|
||||
|
||||
/// Next view, but wrapped around to the first view. May return
|
||||
/// the same view if this is the last view.
|
||||
NextWrapped,
|
||||
|
||||
/// A spatial direction. "Spatial" means that the direction is
|
||||
/// based on the nearest surface in the given direction visually
|
||||
/// as the surfaces are laid out on a 2D grid.
|
||||
Spatial(SpatialDirection),
|
||||
}
|
||||
|
||||
pub type SpatialDirection = Direction;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum Backtrack {
|
||||
Deadend,
|
||||
Backtrack,
|
||||
Result(Handle),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Slot {
|
||||
pub x: f16,
|
||||
pub y: f16,
|
||||
pub width: f16,
|
||||
pub height: f16,
|
||||
}
|
||||
|
||||
impl Slot {
|
||||
fn max_x(self) -> f16 {
|
||||
self.x + self.width
|
||||
}
|
||||
|
||||
fn max_y(self) -> f16 {
|
||||
self.y + self.height
|
||||
}
|
||||
}
|
||||
|
||||
/// Spatial representation of the split tree. See [`SplitTree::spatial`].
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Spatial {
|
||||
/// The slots of the spatial representation in the same order
|
||||
/// as the tree it was created from.
|
||||
pub slots: Vec<Slot>,
|
||||
}
|
||||
|
||||
impl Spatial {
|
||||
pub const EMPTY: Spatial = Spatial { slots: Vec::new() };
|
||||
}
|
||||
|
||||
/// Unsigned integer to f16 (`@floatFromInt`).
|
||||
fn f16_from_usize(v: usize) -> f16 {
|
||||
f16::from_f32(v as f32)
|
||||
}
|
||||
|
||||
fn f16_sqrt(v: f16) -> f16 {
|
||||
f16::from_f32(v.to_f32().sqrt())
|
||||
}
|
||||
|
||||
fn f16_min(a: f16, b: f16) -> f16 {
|
||||
if b < a { b } else { a }
|
||||
}
|
||||
|
||||
fn f16_max(a: f16, b: f16) -> f16 {
|
||||
if b > a { b } else { a }
|
||||
}
|
||||
|
||||
/// See the module documentation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplitTree<V> {
|
||||
/// All the nodes in the tree. Node at index 0 is always the root.
|
||||
nodes: Vec<Node<V>>,
|
||||
|
||||
/// The handle of the zoomed node. A "zoomed" node is one that is
|
||||
/// expected to be made the full size of the split tree. Various
|
||||
/// operations may unzoom (e.g. resize).
|
||||
zoomed: Option<Handle>,
|
||||
}
|
||||
|
||||
impl<V> Default for SplitTree<V> {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> SplitTree<V> {
|
||||
/// An empty tree.
|
||||
#[must_use]
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
nodes: Vec::new(),
|
||||
zoomed: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// All nodes of the tree. Node at index 0 is the root.
|
||||
#[must_use]
|
||||
pub fn nodes(&self) -> &[Node<V>] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
/// The zoomed node, if any.
|
||||
#[must_use]
|
||||
pub fn zoomed(&self) -> Option<Handle> {
|
||||
self.zoomed
|
||||
}
|
||||
|
||||
/// Returns true if this is an empty tree.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
// An empty tree has no nodes.
|
||||
self.nodes.is_empty()
|
||||
}
|
||||
|
||||
/// Returns true if this tree has more than one split (i.e., the root
|
||||
/// is a split node). This is useful for determining if actions like
|
||||
/// resize_split or toggle_split_zoom are performable.
|
||||
#[must_use]
|
||||
pub fn is_split(&self) -> bool {
|
||||
// An empty tree is not split.
|
||||
if self.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// The root node is at index 0. If it's a split, we have multiple splits.
|
||||
matches!(self.nodes[0], Node::Split(_))
|
||||
}
|
||||
|
||||
/// An iterator over all the views in the tree.
|
||||
#[must_use]
|
||||
pub fn iterator(&self) -> Iter<'_, V> {
|
||||
Iter {
|
||||
i: 0,
|
||||
nodes: &self.nodes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the zoomed state to the given node. Assumes the handle
|
||||
/// is valid.
|
||||
pub fn zoom(&mut self, handle: Option<Handle>) {
|
||||
if let Some(v) = handle {
|
||||
assert!(v.idx() < self.nodes.len());
|
||||
}
|
||||
self.zoomed = handle;
|
||||
}
|
||||
|
||||
fn split_node(&self, h: Handle) -> Option<&Split> {
|
||||
match &self.nodes[h.idx()] {
|
||||
Node::Split(s) => Some(s),
|
||||
Node::Leaf(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Goto a view from a certain point in the split tree. Returns `None`
|
||||
/// if the direction results in no visitable view.
|
||||
#[must_use]
|
||||
pub fn goto(&self, from: Handle, to: Goto) -> Option<Handle> {
|
||||
match to {
|
||||
Goto::Previous => self.previous(from),
|
||||
Goto::Next => self.next(from),
|
||||
Goto::PreviousWrapped => self
|
||||
.previous(from)
|
||||
.or_else(|| Some(self.deepest(Side::Right, Handle::ROOT))),
|
||||
Goto::NextWrapped => self
|
||||
.next(from)
|
||||
.or_else(|| Some(self.deepest(Side::Left, Handle::ROOT))),
|
||||
Goto::Spatial(d) => {
|
||||
// Get our spatial representation.
|
||||
let sp = self.spatial();
|
||||
self.nearest_wrapped(&sp, from, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the deepest view in the tree in the given direction.
|
||||
/// This can be used to find the leftmost/rightmost surface within
|
||||
/// a given split structure.
|
||||
#[must_use]
|
||||
pub fn deepest(&self, side: Side, from: Handle) -> Handle {
|
||||
let mut current = from;
|
||||
loop {
|
||||
match &self.nodes[current.idx()] {
|
||||
Node::Leaf(_) => return current,
|
||||
Node::Split(s) => {
|
||||
current = match side {
|
||||
Side::Left => s.left,
|
||||
Side::Right => s.right,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the previous view from the given node handle (which itself
|
||||
/// doesn't need to be a view). If there is no previous (this is the
|
||||
/// most previous view) then this will return `None`.
|
||||
///
|
||||
/// "Previous" is defined as the previous node in an in-order
|
||||
/// traversal of the tree. This isn't a perfect definition and we
|
||||
/// may want to change this to something that better matches a
|
||||
/// spatial view of the tree later.
|
||||
fn previous(&self, from: Handle) -> Option<Handle> {
|
||||
match self.previous_backtrack(from, Handle::ROOT) {
|
||||
Backtrack::Result(v) => Some(v),
|
||||
Backtrack::Backtrack | Backtrack::Deadend => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `previous`, but returns the next view instead.
|
||||
fn next(&self, from: Handle) -> Option<Handle> {
|
||||
match self.next_backtrack(from, Handle::ROOT) {
|
||||
Backtrack::Result(v) => Some(v),
|
||||
Backtrack::Backtrack | Backtrack::Deadend => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Design note: we use a recursive backtracking search because
|
||||
// split trees are never that deep, so we can abuse the stack as
|
||||
// a safe allocator (stack overflow unlikely unless the kernel is
|
||||
// tuned in some really weird way).
|
||||
fn previous_backtrack(&self, from: Handle, current: Handle) -> Backtrack {
|
||||
// If we reached the point that we're trying to find the previous
|
||||
// value of, then we need to backtrack from here.
|
||||
if from == current {
|
||||
return Backtrack::Backtrack;
|
||||
}
|
||||
|
||||
match &self.nodes[current.idx()] {
|
||||
// If we hit a leaf that isn't our target, then deadend.
|
||||
Node::Leaf(_) => Backtrack::Deadend,
|
||||
|
||||
Node::Split(s) => match self.previous_backtrack(from, s.left) {
|
||||
Backtrack::Result(v) => Backtrack::Result(v),
|
||||
|
||||
// Backtrack from the left means we have to continue
|
||||
// backtracking because we can't see what's before the left.
|
||||
Backtrack::Backtrack => Backtrack::Backtrack,
|
||||
|
||||
// If we hit a deadend on the left then let's move right.
|
||||
Backtrack::Deadend => match self.previous_backtrack(from, s.right) {
|
||||
Backtrack::Result(v) => Backtrack::Result(v),
|
||||
|
||||
// Deadend means its not in this split at all since
|
||||
// we already tracked the left.
|
||||
Backtrack::Deadend => Backtrack::Deadend,
|
||||
|
||||
// Backtrack means that its in our left view because
|
||||
// we can see the immediate previous and there MUST
|
||||
// be leaves (we can't have split-only leaves).
|
||||
Backtrack::Backtrack => Backtrack::Result(self.deepest(Side::Right, s.left)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// See previous_backtrack for detailed comments. This is a mirror of that.
|
||||
fn next_backtrack(&self, from: Handle, current: Handle) -> Backtrack {
|
||||
if from == current {
|
||||
return Backtrack::Backtrack;
|
||||
}
|
||||
match &self.nodes[current.idx()] {
|
||||
Node::Leaf(_) => Backtrack::Deadend,
|
||||
Node::Split(s) => match self.next_backtrack(from, s.right) {
|
||||
Backtrack::Result(v) => Backtrack::Result(v),
|
||||
Backtrack::Backtrack => Backtrack::Backtrack,
|
||||
Backtrack::Deadend => match self.next_backtrack(from, s.left) {
|
||||
Backtrack::Result(v) => Backtrack::Result(v),
|
||||
Backtrack::Deadend => Backtrack::Deadend,
|
||||
Backtrack::Backtrack => Backtrack::Result(self.deepest(Side::Left, s.right)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the nearest leaf node (view) in the given direction.
|
||||
/// This does not handle wrapping and will return `None` if there
|
||||
/// is no node in that direction.
|
||||
fn nearest(
|
||||
&self,
|
||||
sp: &Spatial,
|
||||
from: Handle,
|
||||
direction: SpatialDirection,
|
||||
target: Slot,
|
||||
) -> Option<Handle> {
|
||||
let mut result: Option<(Handle, f16)> = None;
|
||||
for (handle, slot) in sp.slots.iter().enumerate() {
|
||||
// Never match ourself
|
||||
if handle == from.idx() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only match leaves
|
||||
if matches!(self.nodes[handle], Node::Split(_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure it is in the proper direction
|
||||
let ok = match direction {
|
||||
Direction::Left => slot.max_x() <= target.x,
|
||||
Direction::Right => slot.x >= target.max_x(),
|
||||
Direction::Up => slot.max_y() <= target.y,
|
||||
Direction::Down => slot.y >= target.max_y(),
|
||||
};
|
||||
if !ok {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track our distance
|
||||
let dx = slot.x - target.x;
|
||||
let dy = slot.y - target.y;
|
||||
let distance = f16_sqrt(dx * dx + dy * dy);
|
||||
|
||||
// If we have a nearest it must be closer.
|
||||
if let Some((_, d)) = result
|
||||
&& distance >= d
|
||||
{
|
||||
continue;
|
||||
}
|
||||
result = Some((Handle::from_idx(handle), distance));
|
||||
}
|
||||
|
||||
result.map(|(h, _)| h)
|
||||
}
|
||||
|
||||
/// Same as nearest but supports wrapping.
|
||||
fn nearest_wrapped(
|
||||
&self,
|
||||
sp: &Spatial,
|
||||
from: Handle,
|
||||
direction: SpatialDirection,
|
||||
) -> Option<Handle> {
|
||||
// If we can find a nearest value without wrapping, then use that.
|
||||
let mut target = sp.slots[from.idx()];
|
||||
if let Some(v) = self.nearest(sp, from, direction, target) {
|
||||
return Some(v);
|
||||
}
|
||||
|
||||
// The spatial grid is normalized to 1x1, so wrapping is modeled
|
||||
// by shifting the target slot by one full grid in the opposite
|
||||
// direction and reusing the same nearest distance logic.
|
||||
// We don't actually modify the grid or spatial representation,
|
||||
// this just fakes it.
|
||||
let one = f16::ONE;
|
||||
assert!(target.x >= f16::ZERO && target.y >= f16::ZERO);
|
||||
assert!(target.max_x() <= one && target.max_y() <= one);
|
||||
match direction {
|
||||
Direction::Left => target.x += one,
|
||||
Direction::Right => target.x -= one,
|
||||
Direction::Up => target.y += one,
|
||||
Direction::Down => target.y -= one,
|
||||
}
|
||||
|
||||
self.nearest(sp, from, direction, target)
|
||||
}
|
||||
|
||||
/// Resize the given node in place. The node MUST be a split (asserted).
|
||||
///
|
||||
/// In general, this is an immutable data structure so this is
|
||||
/// heavily discouraged. However, this is provided for convenience
|
||||
/// and performance reasons where its very important for GUIs to
|
||||
/// update the ratio during a live resize than to redraw the entire
|
||||
/// widget tree.
|
||||
pub fn resize_in_place(&mut self, at: Handle, ratio: f16) {
|
||||
match &mut self.nodes[at.idx()] {
|
||||
Node::Split(s) => s.ratio = ratio,
|
||||
Node::Leaf(_) => panic!("resize_in_place on a leaf node"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of nodes that would be needed to store
|
||||
/// the tree if the target node is removed.
|
||||
fn count_after_removal(&self, current: Handle, target: Handle, acc: usize) -> usize {
|
||||
assert_ne!(current, target);
|
||||
|
||||
match &self.nodes[current.idx()] {
|
||||
// Leaf is simple, always takes one node.
|
||||
Node::Leaf(_) => acc + 1,
|
||||
|
||||
// Split is slightly more complicated. If either side is the
|
||||
// target to remove, then we remove the split node as well
|
||||
// so our count is just the count of the other side.
|
||||
//
|
||||
// If neither side is the target, then we count both sides
|
||||
// and add one to account for the split node itself.
|
||||
Node::Split(s) => {
|
||||
if s.left == target {
|
||||
self.count_after_removal(s.right, target, acc)
|
||||
} else if s.right == target {
|
||||
self.count_after_removal(s.left, target, acc)
|
||||
} else {
|
||||
self.count_after_removal(s.left, target, acc)
|
||||
+ self.count_after_removal(s.right, target, acc)
|
||||
+ 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn weight(&self, from: Handle, layout: Layout, acc: usize) -> usize {
|
||||
match &self.nodes[from.idx()] {
|
||||
Node::Leaf(_) => acc + 1,
|
||||
Node::Split(s) => {
|
||||
if s.layout == layout {
|
||||
self.weight(s.left, layout, acc) + self.weight(s.right, layout, acc)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_parent_split(&self, layout: Layout, from: Handle, current: Handle) -> Backtrack {
|
||||
if from == current {
|
||||
return Backtrack::Backtrack;
|
||||
}
|
||||
match &self.nodes[current.idx()] {
|
||||
Node::Leaf(_) => Backtrack::Deadend,
|
||||
Node::Split(s) => match self.find_parent_split(layout, from, s.left) {
|
||||
Backtrack::Result(v) => Backtrack::Result(v),
|
||||
Backtrack::Backtrack => {
|
||||
if s.layout == layout {
|
||||
Backtrack::Result(current)
|
||||
} else {
|
||||
Backtrack::Backtrack
|
||||
}
|
||||
}
|
||||
Backtrack::Deadend => match self.find_parent_split(layout, from, s.right) {
|
||||
Backtrack::Deadend => Backtrack::Deadend,
|
||||
Backtrack::Result(v) => Backtrack::Result(v),
|
||||
Backtrack::Backtrack => {
|
||||
if s.layout == layout {
|
||||
Backtrack::Result(current)
|
||||
} else {
|
||||
Backtrack::Backtrack
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Spatial representation of the split tree. This can be used to
|
||||
/// better understand the layout of the tree in a 2D space.
|
||||
///
|
||||
/// The bounds of the representation are always based on the total
|
||||
/// 2D space being 1x1. The x/y coordinates and width/height dimensions
|
||||
/// of each individual split and leaf are relative to this.
|
||||
/// This means that the spatial representation is a normalized
|
||||
/// representation of the actual space.
|
||||
///
|
||||
/// The top-left corner of the tree is always (0, 0).
|
||||
///
|
||||
/// We use a normalized form because we can calculate it without
|
||||
/// accessing to the actual rendered view sizes. These actual sizes
|
||||
/// may not be available at various times because GUI toolkits often
|
||||
/// only make them available once they're part of a widget tree and
|
||||
/// a SplitTree can represent views that aren't currently visible.
|
||||
#[must_use]
|
||||
pub fn spatial(&self) -> Spatial {
|
||||
// No nodes, empty spatial representation.
|
||||
if self.nodes.is_empty() {
|
||||
return Spatial::EMPTY;
|
||||
}
|
||||
|
||||
// Get our total dimensions.
|
||||
let (dim_w, dim_h) = self.dimensions(Handle::ROOT);
|
||||
|
||||
// Create our slots which will match our nodes exactly.
|
||||
let zero = Slot {
|
||||
x: f16::ZERO,
|
||||
y: f16::ZERO,
|
||||
width: f16::ZERO,
|
||||
height: f16::ZERO,
|
||||
};
|
||||
let mut slots = vec![zero; self.nodes.len()];
|
||||
slots[0] = Slot {
|
||||
x: f16::ZERO,
|
||||
y: f16::ZERO,
|
||||
width: f16_from_usize(usize::from(dim_w)),
|
||||
height: f16_from_usize(usize::from(dim_h)),
|
||||
};
|
||||
self.fill_spatial_slots(&mut slots, Handle::ROOT);
|
||||
|
||||
// Normalize the dimensions to 1x1 grid.
|
||||
let fw = f16_from_usize(usize::from(dim_w));
|
||||
let fh = f16_from_usize(usize::from(dim_h));
|
||||
for slot in &mut slots {
|
||||
slot.x /= fw;
|
||||
slot.y /= fh;
|
||||
slot.width /= fw;
|
||||
slot.height /= fh;
|
||||
}
|
||||
|
||||
Spatial { slots }
|
||||
}
|
||||
|
||||
fn fill_spatial_slots(&self, slots: &mut [Slot], current_: Handle) {
|
||||
let current = current_.idx();
|
||||
assert!(slots[current].width >= f16::ZERO && slots[current].height >= f16::ZERO);
|
||||
match &self.nodes[current] {
|
||||
// Leaf node, current slot is already filled by caller.
|
||||
Node::Leaf(_) => {}
|
||||
|
||||
Node::Split(s) => {
|
||||
let cur = slots[current];
|
||||
let one = f16::ONE;
|
||||
match s.layout {
|
||||
Layout::Horizontal => {
|
||||
slots[s.left.idx()] = Slot {
|
||||
x: cur.x,
|
||||
y: cur.y,
|
||||
width: cur.width * s.ratio,
|
||||
height: cur.height,
|
||||
};
|
||||
slots[s.right.idx()] = Slot {
|
||||
x: cur.x + cur.width * s.ratio,
|
||||
y: cur.y,
|
||||
width: cur.width * (one - s.ratio),
|
||||
height: cur.height,
|
||||
};
|
||||
}
|
||||
|
||||
Layout::Vertical => {
|
||||
slots[s.left.idx()] = Slot {
|
||||
x: cur.x,
|
||||
y: cur.y,
|
||||
width: cur.width,
|
||||
height: cur.height * s.ratio,
|
||||
};
|
||||
slots[s.right.idx()] = Slot {
|
||||
x: cur.x,
|
||||
y: cur.y + cur.height * s.ratio,
|
||||
width: cur.width,
|
||||
height: cur.height * (one - s.ratio),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.fill_spatial_slots(slots, s.left);
|
||||
self.fill_spatial_slots(slots, s.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the dimensions of the tree starting from the given node.
|
||||
///
|
||||
/// This creates relative dimensions (see Spatial) by assuming each
|
||||
/// leaf is exactly 1x1 unit in size. Returns `(width, height)`.
|
||||
fn dimensions(&self, current: Handle) -> (u16, u16) {
|
||||
match &self.nodes[current.idx()] {
|
||||
Node::Leaf(_) => (1, 1),
|
||||
Node::Split(s) => {
|
||||
let left = self.dimensions(s.left);
|
||||
let right = self.dimensions(s.right);
|
||||
match s.layout {
|
||||
Layout::Horizontal => (left.0 + right.0, left.1.max(right.1)),
|
||||
Layout::Vertical => (left.0.max(right.0), left.1 + right.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: SplitTreeView> SplitTree<V> {
|
||||
/// Initialize a new tree with a single view. The view is referenced
|
||||
/// (cloned) by the tree.
|
||||
#[must_use]
|
||||
pub fn new(view: &V) -> Self {
|
||||
Self {
|
||||
nodes: vec![Node::Leaf(view.clone())],
|
||||
zoomed: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert another tree into this tree at the given node in the
|
||||
/// specified direction. The other tree will be inserted in the
|
||||
/// new direction. For example, if the direction is "right" then
|
||||
/// `insert` is inserted right of the existing node.
|
||||
///
|
||||
/// The previous trees are not modified, but reference counts for the
|
||||
/// views will be increased accordingly for the new tree.
|
||||
pub fn split(
|
||||
&self,
|
||||
at: Handle,
|
||||
direction: Direction,
|
||||
ratio: f16,
|
||||
insert: &Self,
|
||||
) -> Result<Self, SplitError> {
|
||||
// We know we're going to need the sum total of the nodes
|
||||
// between the two trees plus one for the new split node.
|
||||
let total = self.nodes.len() + insert.nodes.len() + 1;
|
||||
if total > usize::from(u16::MAX) {
|
||||
return Err(SplitError::OutOfMemory);
|
||||
}
|
||||
|
||||
// We can copy our nodes exactly as they are, since they're
|
||||
// mostly not changing (only `at` is changing). We can copy the
|
||||
// destination nodes as well directly next to the source nodes. We
|
||||
// just have to go through and offset all the handles in the
|
||||
// destination tree to account for the shift.
|
||||
let mut nodes: Vec<Node<V>> = Vec::with_capacity(total);
|
||||
nodes.extend(self.nodes.iter().cloned());
|
||||
for node in &insert.nodes {
|
||||
nodes.push(match node {
|
||||
Node::Leaf(v) => Node::Leaf(v.clone()),
|
||||
Node::Split(s) => Node::Split(Split {
|
||||
left: s.left.offset(self.nodes.len()),
|
||||
right: s.right.offset(self.nodes.len()),
|
||||
..*s
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Determine our split layout and if we're on the left
|
||||
let (layout, left) = match direction {
|
||||
Direction::Left => (Layout::Horizontal, true),
|
||||
Direction::Right => (Layout::Horizontal, false),
|
||||
Direction::Up => (Layout::Vertical, true),
|
||||
Direction::Down => (Layout::Vertical, false),
|
||||
};
|
||||
|
||||
// Copy our previous value to the end of the nodes list and
|
||||
// create our new split node.
|
||||
let prev = nodes[at.idx()].clone();
|
||||
nodes.push(prev);
|
||||
let last = nodes.len() - 1;
|
||||
let (l, r) = if left {
|
||||
(self.nodes.len(), last)
|
||||
} else {
|
||||
(last, self.nodes.len())
|
||||
};
|
||||
nodes[at.idx()] = Node::Split(Split {
|
||||
layout,
|
||||
ratio,
|
||||
left: Handle::from_idx(l),
|
||||
right: Handle::from_idx(r),
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
nodes,
|
||||
// Splitting always resets zoom state.
|
||||
zoomed: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove a node from the tree.
|
||||
#[must_use]
|
||||
pub fn remove(&self, at: Handle) -> Self {
|
||||
assert!(at.idx() < self.nodes.len());
|
||||
|
||||
// If we're removing node zero then we're clearing the tree.
|
||||
if at == Handle::ROOT {
|
||||
return Self::empty();
|
||||
}
|
||||
|
||||
// Allocate our new nodes list with the number of nodes we'll
|
||||
// need after the removal.
|
||||
let count = self.count_after_removal(Handle::ROOT, at, 0);
|
||||
let mut new_nodes: Vec<Option<Node<V>>> = (0..count).map(|_| None).collect();
|
||||
let mut new_zoomed = None;
|
||||
|
||||
// Traverse the tree and copy all our nodes into place.
|
||||
assert!(self.remove_node(&mut new_nodes, &mut new_zoomed, 0, Handle::ROOT, at) != 0);
|
||||
|
||||
Self {
|
||||
nodes: new_nodes
|
||||
.into_iter()
|
||||
.map(|n| n.expect("all nodes filled by remove"))
|
||||
.collect(),
|
||||
zoomed: new_zoomed,
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_node(
|
||||
&self,
|
||||
new_nodes: &mut [Option<Node<V>>],
|
||||
new_zoomed: &mut Option<Handle>,
|
||||
new_offset: usize,
|
||||
current: Handle,
|
||||
target: Handle,
|
||||
) -> usize {
|
||||
assert_ne!(current, target);
|
||||
|
||||
// If we have a zoomed node and this is it then we migrate it.
|
||||
if let Some(v) = self.zoomed
|
||||
&& v == current
|
||||
{
|
||||
assert!(new_zoomed.is_none());
|
||||
*new_zoomed = Some(Handle::from_idx(new_offset));
|
||||
}
|
||||
|
||||
match &self.nodes[current.idx()] {
|
||||
// Leaf is simple, just copy it over (cloning is the ref).
|
||||
Node::Leaf(view) => {
|
||||
new_nodes[new_offset] = Some(Node::Leaf(view.clone()));
|
||||
1
|
||||
}
|
||||
|
||||
Node::Split(s) => {
|
||||
// If we're removing one of the split node sides then
|
||||
// we remove the split node itself as well and only add
|
||||
// the other (non-removed) side.
|
||||
if s.left == target {
|
||||
return self.remove_node(new_nodes, new_zoomed, new_offset, s.right, target);
|
||||
}
|
||||
if s.right == target {
|
||||
return self.remove_node(new_nodes, new_zoomed, new_offset, s.left, target);
|
||||
}
|
||||
|
||||
// Neither side is being directly removed, so we traverse.
|
||||
let left = self.remove_node(new_nodes, new_zoomed, new_offset + 1, s.left, target);
|
||||
assert!(left != 0);
|
||||
let right = self.remove_node(
|
||||
new_nodes,
|
||||
new_zoomed,
|
||||
new_offset + left + 1,
|
||||
s.right,
|
||||
target,
|
||||
);
|
||||
assert!(right != 0);
|
||||
new_nodes[new_offset] = Some(Node::Split(Split {
|
||||
layout: s.layout,
|
||||
ratio: s.ratio,
|
||||
left: Handle::from_idx(new_offset + 1),
|
||||
right: Handle::from_idx(new_offset + 1 + left),
|
||||
}));
|
||||
|
||||
left + right + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Equalize this node and all its children, returning a new node with splits
|
||||
/// adjusted so that each split's ratio is based on the relative weight
|
||||
/// (number of leaves) of its children.
|
||||
#[must_use]
|
||||
pub fn equalize(&self) -> Self {
|
||||
if self.is_empty() {
|
||||
return Self::empty();
|
||||
}
|
||||
|
||||
// Copy the existing nodes (cloning the views).
|
||||
let mut nodes = self.nodes.clone();
|
||||
|
||||
// Go through and equalize our ratios based on weights.
|
||||
for node in &mut nodes {
|
||||
if let Node::Split(s) = node {
|
||||
let weight_left = self.weight(s.left, s.layout, 0);
|
||||
let weight_right = self.weight(s.right, s.layout, 0);
|
||||
assert!(weight_left > 0);
|
||||
assert!(weight_right > 0);
|
||||
let total_f16 = f16_from_usize(weight_left + weight_right);
|
||||
let weight_left_f16 = f16_from_usize(weight_left);
|
||||
s.ratio = weight_left_f16 / total_f16;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
nodes,
|
||||
zoomed: self.zoomed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the nearest split matching the layout by the given ratio.
|
||||
/// Positive is right and down.
|
||||
///
|
||||
/// The ratio is a signed delta representing the percentage to move
|
||||
/// the divider. The percentage is of the entire grid size, not just
|
||||
/// the specific split size.
|
||||
/// We use the entire grid size because that's what Ghostty's
|
||||
/// `resize_split` keybind does, because it maps to a general human
|
||||
/// understanding of moving a split relative to the entire window
|
||||
/// (generally).
|
||||
///
|
||||
/// For example, a ratio of 0.1 and a layout of `vertical` will find
|
||||
/// the nearest vertical split and move the divider down by 10% of
|
||||
/// the total grid height.
|
||||
///
|
||||
/// If no matching split is found, this does nothing, but will always
|
||||
/// still return a cloned tree.
|
||||
#[must_use]
|
||||
pub fn resize(&self, from: Handle, layout: Layout, ratio: f16) -> Self {
|
||||
assert!(ratio >= f16::from_f32(-1.0) && ratio <= f16::ONE);
|
||||
assert!(!ratio.is_nan());
|
||||
assert!(!ratio.is_infinite());
|
||||
|
||||
// Fast path empty trees.
|
||||
if self.is_empty() {
|
||||
return Self::empty();
|
||||
}
|
||||
|
||||
// From this point forward worst case we return a clone.
|
||||
let mut result = self.clone();
|
||||
|
||||
// Find our nearest parent split node matching the layout.
|
||||
let parent_handle = match self.find_parent_split(layout, from, Handle::ROOT) {
|
||||
Backtrack::Deadend | Backtrack::Backtrack => return result,
|
||||
Backtrack::Result(v) => v,
|
||||
};
|
||||
|
||||
// Get our spatial layout, because we need the dimensions of this
|
||||
// split with regards to the entire grid.
|
||||
let sp = result.spatial();
|
||||
|
||||
// Get the ratio of the split relative to the full grid.
|
||||
let full_ratio = {
|
||||
// Our scale is the amount we need to multiply our individual
|
||||
// ratio by to get the full ratio. Its actually a ratio on its
|
||||
// own but I'm trying to avoid that word: its the ratio of
|
||||
// our spatial width/height to the total.
|
||||
let scale = match layout {
|
||||
Layout::Horizontal => sp.slots[parent_handle.idx()].width / sp.slots[0].width,
|
||||
Layout::Vertical => sp.slots[parent_handle.idx()].height / sp.slots[0].height,
|
||||
};
|
||||
|
||||
let current = result
|
||||
.split_node(parent_handle)
|
||||
.expect("parent is a split")
|
||||
.ratio;
|
||||
current * scale
|
||||
};
|
||||
|
||||
// Set the final new ratio, clamping it to [0, 1]
|
||||
result.resize_in_place(
|
||||
parent_handle,
|
||||
f16_min(f16_max(full_ratio + ratio, f16::ZERO), f16::ONE),
|
||||
);
|
||||
result
|
||||
}
|
||||
|
||||
/// Format the tree in a human-readable format. By default this will
|
||||
/// output a diagram followed by a textual representation.
|
||||
fn format_all(&self, w: &mut dyn fmt::Write) -> fmt::Result {
|
||||
if self.nodes.is_empty() {
|
||||
return w.write_str("empty");
|
||||
}
|
||||
let _ = self.format_diagram(w);
|
||||
self.format_text(w)
|
||||
}
|
||||
|
||||
/// Text representation of the tree.
|
||||
pub fn format_text(&self, w: &mut dyn fmt::Write) -> fmt::Result {
|
||||
if self.nodes.is_empty() {
|
||||
return w.write_str("empty");
|
||||
}
|
||||
self.format_text_inner(w, Handle::ROOT, 0)
|
||||
}
|
||||
|
||||
fn format_text_inner(
|
||||
&self,
|
||||
w: &mut dyn fmt::Write,
|
||||
current: Handle,
|
||||
depth: usize,
|
||||
) -> fmt::Result {
|
||||
for _ in 0..depth {
|
||||
w.write_str(" ")?;
|
||||
}
|
||||
|
||||
if let Some(zoomed) = self.zoomed
|
||||
&& zoomed == current
|
||||
{
|
||||
w.write_str("(zoomed) ")?;
|
||||
}
|
||||
|
||||
match &self.nodes[current.idx()] {
|
||||
Node::Leaf(v) => match v.split_tree_label() {
|
||||
Some(label) => writeln!(w, "leaf: {label}"),
|
||||
None => writeln!(w, "leaf: {}", current.idx()),
|
||||
},
|
||||
|
||||
Node::Split(s) => {
|
||||
let layout = match s.layout {
|
||||
Layout::Horizontal => "horizontal",
|
||||
Layout::Vertical => "vertical",
|
||||
};
|
||||
writeln!(
|
||||
w,
|
||||
"split (layout: {layout}, ratio: {:.2})",
|
||||
s.ratio.to_f32()
|
||||
)?;
|
||||
self.format_text_inner(w, s.left, depth + 1)?;
|
||||
self.format_text_inner(w, s.right, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ASCII-art diagram of the tree.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn format_diagram(&self, w: &mut dyn fmt::Write) -> fmt::Result {
|
||||
if self.nodes.is_empty() {
|
||||
return w.write_str("empty");
|
||||
}
|
||||
|
||||
// Get our spatial representation.
|
||||
let sp = {
|
||||
let sp = self.spatial();
|
||||
|
||||
// Scale our spatial representation to have minimum width/height 1.
|
||||
let mut min_w = f16::ONE;
|
||||
let mut min_h = f16::ONE;
|
||||
for slot in &sp.slots {
|
||||
if slot.width > f16::ZERO {
|
||||
min_w = f16_min(min_w, slot.width);
|
||||
}
|
||||
if slot.height > f16::ZERO {
|
||||
min_h = f16_min(min_h, slot.height);
|
||||
}
|
||||
}
|
||||
|
||||
let ratio_w = f16::ONE / min_w;
|
||||
let ratio_h = f16::ONE / min_h;
|
||||
let mut slots = sp.slots.clone();
|
||||
for slot in &mut slots {
|
||||
slot.x *= ratio_w;
|
||||
slot.y *= ratio_h;
|
||||
slot.width *= ratio_w;
|
||||
slot.height *= ratio_h;
|
||||
}
|
||||
|
||||
Spatial { slots }
|
||||
};
|
||||
|
||||
let label_of = |handle: usize, node: &Node<V>| -> Option<String> {
|
||||
match node {
|
||||
Node::Split(_) => None,
|
||||
Node::Leaf(v) => Some(v.split_tree_label().unwrap_or_else(|| handle.to_string())),
|
||||
}
|
||||
};
|
||||
|
||||
// The width we need for the largest label.
|
||||
let max_label_width: usize = {
|
||||
let mut max = 0;
|
||||
let mut any_label = false;
|
||||
for node in &self.nodes {
|
||||
if let Node::Leaf(v) = node
|
||||
&& let Some(label) = v.split_tree_label()
|
||||
{
|
||||
any_label = true;
|
||||
max = max.max(label.len());
|
||||
}
|
||||
}
|
||||
if any_label {
|
||||
max
|
||||
} else {
|
||||
(sp.slots.len().ilog10() + 1) as usize
|
||||
}
|
||||
};
|
||||
|
||||
// We need space for whitespace and ASCII art so add that.
|
||||
// Border + whitespace + label + whitespace + border.
|
||||
let cell_width = 2 + max_label_width + 2;
|
||||
// Border + label + border. No whitespace needed on the vertical axis.
|
||||
let cell_height = 1 + 1 + 1;
|
||||
|
||||
// Make a grid that can fit our entire ASCII diagram. We know
|
||||
// the width/height based on node 0.
|
||||
let mut grid: Vec<Vec<u8>> = {
|
||||
// Get our initial width/height. Each leaf is 1x1 in this.
|
||||
// We round up for this because partial widths/heights should
|
||||
// take up an extra cell.
|
||||
let mut width = sp.slots[0].width.to_f32().ceil() as usize;
|
||||
let mut height = sp.slots[0].height.to_f32().ceil() as usize;
|
||||
|
||||
width *= cell_width;
|
||||
height *= cell_height;
|
||||
|
||||
(0..height)
|
||||
.map(|_| {
|
||||
let mut row = vec![b' '; width + 1];
|
||||
row[width] = b'\n';
|
||||
row
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Draw each node
|
||||
for (handle, slot) in sp.slots.iter().enumerate() {
|
||||
// We only draw leaf nodes. Splits are only used for layout.
|
||||
let node = &self.nodes[handle];
|
||||
let Some(label) = label_of(handle, node) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// If our width/height is zero then we skip this.
|
||||
if slot.width == f16::ZERO || slot.height == f16::ZERO {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut x = slot.x.to_f32().floor() as usize;
|
||||
let mut y = slot.y.to_f32().floor() as usize;
|
||||
let mut width = slot.width.to_f32().floor().max(1.0) as usize;
|
||||
let mut height = slot.height.to_f32().floor().max(1.0) as usize;
|
||||
x *= cell_width;
|
||||
y *= cell_height;
|
||||
width *= cell_width;
|
||||
height *= cell_height;
|
||||
|
||||
// Top border
|
||||
{
|
||||
let top = &mut grid[y][x..x + width];
|
||||
top[0] = b'+';
|
||||
for c in &mut top[1..width - 1] {
|
||||
*c = b'-';
|
||||
}
|
||||
top[width - 1] = b'+';
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
{
|
||||
let bottom = &mut grid[y + height - 1][x..x + width];
|
||||
bottom[0] = b'+';
|
||||
for c in &mut bottom[1..width - 1] {
|
||||
*c = b'-';
|
||||
}
|
||||
bottom[width - 1] = b'+';
|
||||
}
|
||||
|
||||
// Left and right borders
|
||||
for row in &mut grid[y + 1..y + height - 1] {
|
||||
row[x] = b'|';
|
||||
}
|
||||
for row in &mut grid[y + 1..y + height - 1] {
|
||||
row[x + width - 1] = b'|';
|
||||
}
|
||||
|
||||
// Draw the handle in the center
|
||||
let x_mid = width / 2 + x;
|
||||
let y_mid = height / 2 + y;
|
||||
let label_width = label.len();
|
||||
let label_start = x_mid - label_width / 2;
|
||||
let row = &mut grid[y_mid][label_start..];
|
||||
if label.len() > row.len() {
|
||||
return Err(fmt::Error);
|
||||
}
|
||||
row[..label.len()].copy_from_slice(label.as_bytes());
|
||||
}
|
||||
|
||||
// Output every row
|
||||
for row in &grid {
|
||||
// We currently have a bug in our height calculation that
|
||||
// results in trailing blank lines. Ignore those. We should
|
||||
// really fix our height calculation instead. If someone wants
|
||||
// to do that just remove this line and see the tests that fail
|
||||
// and go from there.
|
||||
if row[0] == b' ' {
|
||||
break;
|
||||
}
|
||||
w.write_str(&String::from_utf8_lossy(row))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The diagram as a string (the Zig `{f}` of `std.fmt.alt(t, .formatDiagram)`).
|
||||
#[must_use]
|
||||
pub fn diagram_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
let _ = self.format_diagram(&mut s);
|
||||
s
|
||||
}
|
||||
|
||||
/// The text representation as a string (`.formatText`).
|
||||
#[must_use]
|
||||
pub fn text_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
let _ = self.format_text(&mut s);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: SplitTreeView> fmt::Display for SplitTree<V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut s = String::new();
|
||||
self.format_all(&mut s)?;
|
||||
f.write_str(&s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestView {
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
impl SplitTreeView for TestView {
|
||||
fn split_tree_label(&self) -> Option<String> {
|
||||
Some(self.label.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
type TestTree = SplitTree<TestView>;
|
||||
|
||||
fn v(label: &'static str) -> TestView {
|
||||
TestView { label }
|
||||
}
|
||||
|
||||
fn h(f: f32) -> f16 {
|
||||
f16::from_f32(f)
|
||||
}
|
||||
|
||||
fn find(t: &TestTree, label: &str) -> Handle {
|
||||
t.iterator()
|
||||
.find(|e| e.view.label == label)
|
||||
.expect("not found")
|
||||
.handle
|
||||
}
|
||||
|
||||
fn leaf_label(t: &TestTree, handle: Handle) -> &'static str {
|
||||
match &t.nodes()[handle.idx()] {
|
||||
Node::Leaf(v) => v.label,
|
||||
Node::Split(_) => panic!("not a leaf"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_is_split() {
|
||||
// Empty tree should not be split
|
||||
let empty = TestTree::empty();
|
||||
assert!(!empty.is_split());
|
||||
|
||||
// Single node tree should not be split
|
||||
let single = TestTree::new(&v("A"));
|
||||
assert!(!single.is_split());
|
||||
|
||||
// Split tree should be split
|
||||
let tree2 = TestTree::new(&v("B"));
|
||||
let split = single
|
||||
.split(Handle::ROOT, Direction::Right, h(0.5), &tree2)
|
||||
.unwrap();
|
||||
assert!(split.is_split());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_empty_tree() {
|
||||
let t = TestTree::empty();
|
||||
assert_eq!(t.to_string(), "empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_single_node() {
|
||||
let t = TestTree::new(&v("A"));
|
||||
assert_eq!(t.diagram_string(), "+---+\n| A |\n+---+\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_horizontal() {
|
||||
let t1 = TestTree::new(&v("A"));
|
||||
let t2 = TestTree::new(&v("B"));
|
||||
let t3 = t1
|
||||
.split(Handle::ROOT, Direction::Right, h(0.5), &t2)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
t3.to_string(),
|
||||
"+---++---+\n\
|
||||
| A || B |\n\
|
||||
+---++---+\n\
|
||||
split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: A\n\
|
||||
\x20 leaf: B\n"
|
||||
);
|
||||
|
||||
// Split right at B
|
||||
let tc = TestTree::new(&v("C"));
|
||||
let t4 = t3
|
||||
.split(find(&t3, "B"), Direction::Right, h(0.5), &tc)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t4.to_string(),
|
||||
"+--------++---++---+\n\
|
||||
| A || B || C |\n\
|
||||
+--------++---++---+\n\
|
||||
split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: A\n\
|
||||
\x20 split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: B\n\
|
||||
\x20 leaf: C\n"
|
||||
);
|
||||
|
||||
// Split right at C
|
||||
let td = TestTree::new(&v("D"));
|
||||
let t5 = t4
|
||||
.split(find(&t4, "C"), Direction::Right, h(0.5), &td)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t5.to_string(),
|
||||
"+------------------++--------++---++---+\n\
|
||||
| A || B || C || D |\n\
|
||||
+------------------++--------++---++---+\n\
|
||||
split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: A\n\
|
||||
\x20 split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: B\n\
|
||||
\x20 split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: C\n\
|
||||
\x20 leaf: D\n"
|
||||
);
|
||||
|
||||
// Find "previous" from D back.
|
||||
let mut current = b'D';
|
||||
while current != b'A' {
|
||||
let label = (current as char).to_string();
|
||||
let handle = t5.previous(find(&t5, &label)).unwrap();
|
||||
assert_eq!(leaf_label(&t5, handle).as_bytes(), &[current - 1]);
|
||||
current -= 1;
|
||||
}
|
||||
assert!(t5.previous(find(&t5, "A")).is_none());
|
||||
|
||||
// Find "next" from A forward.
|
||||
let mut current = b'A';
|
||||
while current != b'D' {
|
||||
let label = (current as char).to_string();
|
||||
let handle = t5.next(find(&t5, &label)).unwrap();
|
||||
assert_eq!(leaf_label(&t5, handle).as_bytes(), &[current + 1]);
|
||||
current += 1;
|
||||
}
|
||||
assert!(t5.next(find(&t5, "D")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_vertical() {
|
||||
let t1 = TestTree::new(&v("A"));
|
||||
let t2 = TestTree::new(&v("B"));
|
||||
let t3 = t1
|
||||
.split(Handle::ROOT, Direction::Down, h(0.5), &t2)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t3.diagram_string(),
|
||||
"+---+\n| A |\n+---+\n+---+\n| B |\n+---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
fn ab_split(dir: Direction, ratio: f32) -> TestTree {
|
||||
let t1 = TestTree::new(&v("A"));
|
||||
let t2 = TestTree::new(&v("B"));
|
||||
t1.split(Handle::ROOT, dir, h(ratio), &t2).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_horizontal_with_zero_ratio() {
|
||||
assert_eq!(
|
||||
ab_split(Direction::Right, 0.0).diagram_string(),
|
||||
"+---+\n| B |\n+---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_vertical_with_zero_ratio() {
|
||||
assert_eq!(
|
||||
ab_split(Direction::Down, 0.0).diagram_string(),
|
||||
"+---+\n| B |\n+---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_horizontal_with_full_width() {
|
||||
assert_eq!(
|
||||
ab_split(Direction::Right, 1.0).diagram_string(),
|
||||
"+---+\n| A |\n+---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_vertical_with_full_width() {
|
||||
assert_eq!(
|
||||
ab_split(Direction::Down, 1.0).diagram_string(),
|
||||
"+---+\n| A |\n+---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_remove_leaf() {
|
||||
let t3 = ab_split(Direction::Right, 0.5);
|
||||
let t4 = t3.remove(find(&t3, "A"));
|
||||
assert_eq!(t4.diagram_string(), "+---+\n| B |\n+---+\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_twice_remove_intermediary() {
|
||||
let t3 = TestTree::new(&v("C"));
|
||||
|
||||
// A | B horizontal.
|
||||
let split1 = ab_split(Direction::Right, 0.5);
|
||||
|
||||
// Insert C below that.
|
||||
let split2 = split1
|
||||
.split(Handle::ROOT, Direction::Down, h(0.5), &t3)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
split2.diagram_string(),
|
||||
"+---++---+\n\
|
||||
| A || B |\n\
|
||||
+---++---+\n\
|
||||
+--------+\n\
|
||||
| C |\n\
|
||||
+--------+\n"
|
||||
);
|
||||
|
||||
// Remove "B"
|
||||
let split3 = split2.remove(find(&split2, "B"));
|
||||
assert_eq!(
|
||||
split3.diagram_string(),
|
||||
"+---+\n| A |\n+---+\n+---+\n| C |\n+---+\n"
|
||||
);
|
||||
|
||||
// Remove every node from split2 (our most complex one), which should
|
||||
// never crash. We don't test the result is correct, this just verifies
|
||||
// we don't hit any assertion failures.
|
||||
for i in 0..split2.nodes().len() {
|
||||
let _ = split2.remove(Handle::from_idx(i));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_spatial_goto() {
|
||||
let t1 = TestTree::new(&v("A"));
|
||||
let t2 = TestTree::new(&v("B"));
|
||||
let t3 = TestTree::new(&v("C"));
|
||||
let t4 = TestTree::new(&v("D"));
|
||||
|
||||
// A | B horizontal
|
||||
let split_ab = t1
|
||||
.split(Handle::ROOT, Direction::Right, h(0.5), &t2)
|
||||
.unwrap();
|
||||
|
||||
// A | C vertical
|
||||
let split_ac = split_ab
|
||||
.split(find(&split_ab, "A"), Direction::Down, h(0.8), &t3)
|
||||
.unwrap();
|
||||
|
||||
// B | D vertical
|
||||
let split = split_ac
|
||||
.split(find(&split_ab, "B"), Direction::Down, h(0.3), &t4)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
split.diagram_string(),
|
||||
"+---++---+\n\
|
||||
| || B |\n\
|
||||
| |+---+\n\
|
||||
| |+---+\n\
|
||||
| A || |\n\
|
||||
| || |\n\
|
||||
| || |\n\
|
||||
| || D |\n\
|
||||
+---+| |\n\
|
||||
+---+| |\n\
|
||||
| C || |\n\
|
||||
+---++---+\n"
|
||||
);
|
||||
|
||||
let go = |from: &str, dir: Direction| {
|
||||
let target = split
|
||||
.goto(find(&split, from), Goto::Spatial(dir))
|
||||
.expect("target");
|
||||
leaf_label(&split, target)
|
||||
};
|
||||
|
||||
// Spatial C => right
|
||||
assert_eq!(go("C", Direction::Right), "D");
|
||||
// Spatial D => left
|
||||
assert_eq!(go("D", Direction::Left), "A");
|
||||
// Spatial A => left (wrapped)
|
||||
assert_eq!(go("A", Direction::Left), "B");
|
||||
// Spatial B => right (wrapped)
|
||||
assert_eq!(go("B", Direction::Right), "A");
|
||||
// Spatial C => down (wrapped)
|
||||
assert_eq!(go("C", Direction::Down), "A");
|
||||
|
||||
// Equalize
|
||||
let equal = split.equalize();
|
||||
assert_eq!(
|
||||
equal.diagram_string(),
|
||||
"+---++---+\n\
|
||||
| A || B |\n\
|
||||
+---++---+\n\
|
||||
+---++---+\n\
|
||||
| C || D |\n\
|
||||
+---++---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_resize() {
|
||||
let split = ab_split(Direction::Right, 0.5);
|
||||
assert_eq!(
|
||||
split.diagram_string(),
|
||||
"+---++---+\n| A || B |\n+---++---+\n"
|
||||
);
|
||||
|
||||
// Resize
|
||||
{
|
||||
let resized = split.resize(find(&split, "B"), Layout::Horizontal, h(0.25));
|
||||
assert_eq!(
|
||||
resized.diagram_string(),
|
||||
"+-------------++---+\n\
|
||||
| A || B |\n\
|
||||
+-------------++---+\n"
|
||||
);
|
||||
}
|
||||
|
||||
// Resize the other direction (negative ratio)
|
||||
{
|
||||
let resized = split.resize(find(&split, "B"), Layout::Horizontal, h(-0.25));
|
||||
assert_eq!(
|
||||
resized.diagram_string(),
|
||||
"+---++-------------+\n\
|
||||
| A || B |\n\
|
||||
+---++-------------+\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_clone_empty_tree() {
|
||||
let t = TestTree::empty();
|
||||
let t2 = t.clone();
|
||||
assert_eq!(t2.to_string(), "empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_zoom() {
|
||||
let mut split = ab_split(Direction::Right, 0.5);
|
||||
let b = find(&split, "B");
|
||||
split.zoom(Some(b));
|
||||
|
||||
let expected = "split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: A\n\
|
||||
\x20 (zoomed) leaf: B\n";
|
||||
assert_eq!(split.text_string(), expected);
|
||||
|
||||
// Clone preserves zoom
|
||||
let clone = split.clone();
|
||||
assert_eq!(clone.text_string(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_resets_zoom() {
|
||||
let mut t1 = TestTree::new(&v("A"));
|
||||
let t2 = TestTree::new(&v("B"));
|
||||
|
||||
// Zoom A
|
||||
let a = find(&t1, "A");
|
||||
t1.zoom(Some(a));
|
||||
|
||||
let split = t1
|
||||
.split(Handle::ROOT, Direction::Right, h(0.5), &t2)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
split.text_string(),
|
||||
"split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: A\n\
|
||||
\x20 leaf: B\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_remove_and_zoom() {
|
||||
let mut split = ab_split(Direction::Right, 0.5);
|
||||
let a = find(&split, "A");
|
||||
split.zoom(Some(a));
|
||||
|
||||
// Remove A, should unzoom
|
||||
{
|
||||
let removed = split.remove(find(&split, "A"));
|
||||
assert!(removed.zoomed().is_none());
|
||||
assert_eq!(removed.text_string(), "leaf: B\n");
|
||||
}
|
||||
|
||||
// Remove B, should keep zoom
|
||||
{
|
||||
let removed = split.remove(find(&split, "B"));
|
||||
assert_eq!(removed.text_string(), "(zoomed) leaf: A\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Extra tests beyond the Zig suite.
|
||||
|
||||
#[test]
|
||||
fn split_tree_goto_linear() {
|
||||
let mut t = TestTree::new(&v("A"));
|
||||
for l in ["B", "C"] {
|
||||
let n = TestTree::new(&v(l));
|
||||
let at = find(&t, if l == "B" { "A" } else { "B" });
|
||||
t = t.split(at, Direction::Right, h(0.5), &n).unwrap();
|
||||
}
|
||||
let a = find(&t, "A");
|
||||
let c = find(&t, "C");
|
||||
assert_eq!(t.goto(a, Goto::Previous), None);
|
||||
assert_eq!(t.goto(c, Goto::Next), None);
|
||||
assert_eq!(t.goto(a, Goto::PreviousWrapped), Some(c));
|
||||
assert_eq!(t.goto(c, Goto::NextWrapped), Some(a));
|
||||
assert_eq!(leaf_label(&t, t.goto(a, Goto::Next).unwrap()), "B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_split_too_many_nodes() {
|
||||
// 2^15 each would exceed u16 handles.
|
||||
let big = SplitTree {
|
||||
nodes: vec![Node::Leaf(v("X")); 40_000],
|
||||
zoomed: None,
|
||||
};
|
||||
let err = big
|
||||
.split(Handle::ROOT, Direction::Right, h(0.5), &big)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, SplitError::OutOfMemory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_default_label_uses_handle() {
|
||||
#[derive(Clone)]
|
||||
struct Plain;
|
||||
impl SplitTreeView for Plain {}
|
||||
let a = SplitTree::new(&Plain);
|
||||
let b = SplitTree::new(&Plain);
|
||||
let s = a.split(Handle::ROOT, Direction::Right, h(0.5), &b).unwrap();
|
||||
assert_eq!(
|
||||
s.to_string(),
|
||||
"+---++---+\n\
|
||||
| 2 || 1 |\n\
|
||||
+---++---+\n\
|
||||
split (layout: horizontal, ratio: 0.50)\n\
|
||||
\x20 leaf: 2\n\
|
||||
\x20 leaf: 1\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_tree_ref_counts_follow_clones() {
|
||||
use std::rc::Rc;
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
struct Rcv(Rc<()>);
|
||||
impl SplitTreeView for Rcv {}
|
||||
let rc = Rc::new(());
|
||||
let a = SplitTree::new(&Rcv(rc.clone()));
|
||||
assert_eq!(Rc::strong_count(&rc), 2);
|
||||
let b = SplitTree::new(&Rcv(rc.clone()));
|
||||
let s = a.split(Handle::ROOT, Direction::Right, h(0.5), &b).unwrap();
|
||||
assert_eq!(Rc::strong_count(&rc), 5);
|
||||
drop(a);
|
||||
drop(b);
|
||||
assert_eq!(Rc::strong_count(&rc), 3);
|
||||
drop(s);
|
||||
assert_eq!(Rc::strong_count(&rc), 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user