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
1707 lines
55 KiB
Rust
1707 lines
55 KiB
Rust
//! `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);
|
|
}
|
|
}
|