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:
ddidderr committed 2026-10-11 21:54:57 +02:00
1 parent e724645057
commit 3323c3f525
31 files changed
+18468 -3

No files matched your search

+2
View File
@@ -5,6 +5,8 @@ edition.workspace = true
license.workspace = true
[dependencies]
half = "2.7.1"
thiserror = "2.0.21"
[lints]
@@ -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);
}
}
+789
View File
@@ -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]);
}
}
+534
View File
@@ -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]));
}
}
+40 -1
View File
@@ -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;
+389
View File
@@ -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));
}
}
+1706
View File
@@ -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);
}
}