mirror of
https://github.com/helix-editor/helix.git
synced 2024-12-18 22:11:55 +04:00
970 lines
32 KiB
Rust
970 lines
32 KiB
Rust
use crate::{graphics::Rect, View, ViewId};
|
|
use slotmap::HopSlotMap;
|
|
|
|
// the dimensions are recomputed on window resize/tree change.
|
|
//
|
|
#[derive(Debug)]
|
|
pub struct Tree {
|
|
root: ViewId,
|
|
// (container, index inside the container)
|
|
pub focus: ViewId,
|
|
// fullscreen: bool,
|
|
area: Rect,
|
|
|
|
nodes: HopSlotMap<ViewId, Node>,
|
|
|
|
// used for traversals
|
|
stack: Vec<(ViewId, Rect)>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Node {
|
|
parent: ViewId,
|
|
content: Content,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Content {
|
|
View(Box<View>),
|
|
Container(Box<Container>),
|
|
}
|
|
|
|
impl Node {
|
|
pub fn container(layout: Layout) -> Self {
|
|
Self {
|
|
parent: ViewId::default(),
|
|
content: Content::Container(Box::new(Container::new(layout))),
|
|
}
|
|
}
|
|
|
|
pub fn view(view: View) -> Self {
|
|
Self {
|
|
parent: ViewId::default(),
|
|
content: Content::View(Box::new(view)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Layout {
|
|
Horizontal,
|
|
Vertical,
|
|
// could explore stacked/tabbed
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum Direction {
|
|
Up,
|
|
Down,
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Container {
|
|
layout: Layout,
|
|
children: Vec<ViewId>,
|
|
area: Rect,
|
|
}
|
|
|
|
impl Container {
|
|
pub fn new(layout: Layout) -> Self {
|
|
Self {
|
|
layout,
|
|
children: Vec::new(),
|
|
area: Rect::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Container {
|
|
fn default() -> Self {
|
|
Self::new(Layout::Vertical)
|
|
}
|
|
}
|
|
|
|
impl Tree {
|
|
pub fn new(area: Rect) -> Self {
|
|
let root = Node::container(Layout::Vertical);
|
|
|
|
let mut nodes = HopSlotMap::with_key();
|
|
let root = nodes.insert(root);
|
|
|
|
// root is it's own parent
|
|
nodes[root].parent = root;
|
|
|
|
Self {
|
|
root,
|
|
focus: root,
|
|
// fullscreen: false,
|
|
area,
|
|
nodes,
|
|
stack: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn insert(&mut self, view: View) -> ViewId {
|
|
let focus = self.focus;
|
|
let parent = self.nodes[focus].parent;
|
|
let mut node = Node::view(view);
|
|
node.parent = parent;
|
|
let node = self.nodes.insert(node);
|
|
self.get_mut(node).id = node;
|
|
|
|
let container = match &mut self.nodes[parent] {
|
|
Node {
|
|
content: Content::Container(container),
|
|
..
|
|
} => container,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
// insert node after the current item if there is children already
|
|
let pos = if container.children.is_empty() {
|
|
0
|
|
} else {
|
|
let pos = container
|
|
.children
|
|
.iter()
|
|
.position(|&child| child == focus)
|
|
.unwrap();
|
|
pos + 1
|
|
};
|
|
|
|
container.children.insert(pos, node);
|
|
// focus the new node
|
|
self.focus = node;
|
|
|
|
// recalculate all the sizes
|
|
self.recalculate();
|
|
|
|
node
|
|
}
|
|
|
|
pub fn split(&mut self, view: View, layout: Layout) -> ViewId {
|
|
let focus = self.focus;
|
|
let parent = self.nodes[focus].parent;
|
|
|
|
let node = Node::view(view);
|
|
let node = self.nodes.insert(node);
|
|
self.get_mut(node).id = node;
|
|
|
|
let container = match &mut self.nodes[parent] {
|
|
Node {
|
|
content: Content::Container(container),
|
|
..
|
|
} => container,
|
|
_ => unreachable!(),
|
|
};
|
|
if container.layout == layout {
|
|
// insert node after the current item if there is children already
|
|
let pos = if container.children.is_empty() {
|
|
0
|
|
} else {
|
|
let pos = container
|
|
.children
|
|
.iter()
|
|
.position(|&child| child == focus)
|
|
.unwrap();
|
|
pos + 1
|
|
};
|
|
container.children.insert(pos, node);
|
|
self.nodes[node].parent = parent;
|
|
} else {
|
|
let mut split = Node::container(layout);
|
|
split.parent = parent;
|
|
let split = self.nodes.insert(split);
|
|
|
|
let container = match &mut self.nodes[split] {
|
|
Node {
|
|
content: Content::Container(container),
|
|
..
|
|
} => container,
|
|
_ => unreachable!(),
|
|
};
|
|
container.children.push(focus);
|
|
container.children.push(node);
|
|
self.nodes[focus].parent = split;
|
|
self.nodes[node].parent = split;
|
|
|
|
let container = match &mut self.nodes[parent] {
|
|
Node {
|
|
content: Content::Container(container),
|
|
..
|
|
} => container,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
let pos = container
|
|
.children
|
|
.iter()
|
|
.position(|&child| child == focus)
|
|
.unwrap();
|
|
|
|
// replace focus on parent with split
|
|
container.children[pos] = split;
|
|
}
|
|
|
|
// focus the new node
|
|
self.focus = node;
|
|
|
|
// recalculate all the sizes
|
|
self.recalculate();
|
|
|
|
node
|
|
}
|
|
|
|
/// Get a mutable reference to a [Container] by index.
|
|
/// # Panics
|
|
/// Panics if `index` is not in self.nodes, or if the node's content is not a [Content::Container].
|
|
fn container_mut(&mut self, index: ViewId) -> &mut Container {
|
|
match &mut self.nodes[index] {
|
|
Node {
|
|
content: Content::Container(container),
|
|
..
|
|
} => container,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn remove_or_replace(&mut self, child: ViewId, replacement: Option<ViewId>) {
|
|
let parent = self.nodes[child].parent;
|
|
|
|
self.nodes.remove(child);
|
|
|
|
let container = self.container_mut(parent);
|
|
let pos = container
|
|
.children
|
|
.iter()
|
|
.position(|&item| item == child)
|
|
.unwrap();
|
|
|
|
if let Some(new) = replacement {
|
|
container.children[pos] = new;
|
|
self.nodes[new].parent = parent;
|
|
} else {
|
|
container.children.remove(pos);
|
|
}
|
|
}
|
|
|
|
pub fn remove(&mut self, index: ViewId) {
|
|
if self.focus == index {
|
|
// focus on something else
|
|
self.focus = self.prev();
|
|
}
|
|
|
|
let parent = self.nodes[index].parent;
|
|
let parent_is_root = parent == self.root;
|
|
|
|
self.remove_or_replace(index, None);
|
|
|
|
let parent_container = self.container_mut(parent);
|
|
if parent_container.children.len() == 1 && !parent_is_root {
|
|
// Lets merge the only child back to its grandparent so that Views
|
|
// are equally spaced.
|
|
let sibling = parent_container.children.pop().unwrap();
|
|
self.remove_or_replace(parent, Some(sibling));
|
|
}
|
|
|
|
self.recalculate()
|
|
}
|
|
|
|
pub fn views(&self) -> impl Iterator<Item = (&View, bool)> {
|
|
let focus = self.focus;
|
|
self.nodes.iter().filter_map(move |(key, node)| match node {
|
|
Node {
|
|
content: Content::View(view),
|
|
..
|
|
} => Some((view.as_ref(), focus == key)),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
pub fn views_mut(&mut self) -> impl Iterator<Item = (&mut View, bool)> {
|
|
let focus = self.focus;
|
|
self.nodes
|
|
.iter_mut()
|
|
.filter_map(move |(key, node)| match node {
|
|
Node {
|
|
content: Content::View(view),
|
|
..
|
|
} => Some((view.as_mut(), focus == key)),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
/// Get reference to a [View] by index.
|
|
/// # Panics
|
|
///
|
|
/// Panics if `index` is not in self.nodes, or if the node's content is not [Content::View]. This can be checked with [Self::contains].
|
|
pub fn get(&self, index: ViewId) -> &View {
|
|
self.try_get(index).unwrap()
|
|
}
|
|
|
|
/// Try to get reference to a [View] by index. Returns `None` if node content is not a [`Content::View`].
|
|
///
|
|
/// Does not panic if the view does not exists anymore.
|
|
pub fn try_get(&self, index: ViewId) -> Option<&View> {
|
|
match self.nodes.get(index) {
|
|
Some(Node {
|
|
content: Content::View(view),
|
|
..
|
|
}) => Some(view),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Get a mutable reference to a [View] by index.
|
|
/// # Panics
|
|
///
|
|
/// Panics if `index` is not in self.nodes, or if the node's content is not [Content::View]. This can be checked with [Self::contains].
|
|
pub fn get_mut(&mut self, index: ViewId) -> &mut View {
|
|
match &mut self.nodes[index] {
|
|
Node {
|
|
content: Content::View(view),
|
|
..
|
|
} => view,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Check if tree contains a [Node] with a given index.
|
|
pub fn contains(&self, index: ViewId) -> bool {
|
|
self.nodes.contains_key(index)
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
match &self.nodes[self.root] {
|
|
Node {
|
|
content: Content::Container(container),
|
|
..
|
|
} => container.children.is_empty(),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
pub fn resize(&mut self, area: Rect) -> bool {
|
|
if self.area != area {
|
|
self.area = area;
|
|
self.recalculate();
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn recalculate(&mut self) {
|
|
if self.is_empty() {
|
|
// There are no more views, so the tree should focus itself again.
|
|
self.focus = self.root;
|
|
|
|
return;
|
|
}
|
|
|
|
self.stack.push((self.root, self.area));
|
|
|
|
// take the area
|
|
// fetch the node
|
|
// a) node is view, give it whole area
|
|
// b) node is container, calculate areas for each child and push them on the stack
|
|
|
|
while let Some((key, area)) = self.stack.pop() {
|
|
let node = &mut self.nodes[key];
|
|
|
|
match &mut node.content {
|
|
Content::View(view) => {
|
|
// debug!!("setting view area {:?}", area);
|
|
view.area = area;
|
|
} // TODO: call f()
|
|
Content::Container(container) => {
|
|
// debug!!("setting container area {:?}", area);
|
|
container.area = area;
|
|
|
|
match container.layout {
|
|
Layout::Horizontal => {
|
|
let len = container.children.len();
|
|
|
|
let height = area.height / len as u16;
|
|
|
|
let mut child_y = area.y;
|
|
|
|
for (i, child) in container.children.iter().enumerate() {
|
|
let mut area = Rect::new(
|
|
container.area.x,
|
|
child_y,
|
|
container.area.width,
|
|
height,
|
|
);
|
|
child_y += height;
|
|
|
|
// last child takes the remaining width because we can get uneven
|
|
// space from rounding
|
|
if i == len - 1 {
|
|
area.height = container.area.y + container.area.height - area.y;
|
|
}
|
|
|
|
self.stack.push((*child, area));
|
|
}
|
|
}
|
|
Layout::Vertical => {
|
|
let len = container.children.len();
|
|
let len_u16 = len as u16;
|
|
|
|
let inner_gap = 1u16;
|
|
let total_gap = inner_gap * len_u16.saturating_sub(2);
|
|
|
|
let used_area = area.width.saturating_sub(total_gap);
|
|
let width = used_area / len_u16;
|
|
|
|
let mut child_x = area.x;
|
|
|
|
for (i, child) in container.children.iter().enumerate() {
|
|
let mut area = Rect::new(
|
|
child_x,
|
|
container.area.y,
|
|
width,
|
|
container.area.height,
|
|
);
|
|
child_x += width + inner_gap;
|
|
|
|
// last child takes the remaining width because we can get uneven
|
|
// space from rounding
|
|
if i == len - 1 {
|
|
area.width = container.area.x + container.area.width - area.x;
|
|
}
|
|
|
|
self.stack.push((*child, area));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn traverse(&self) -> Traverse {
|
|
Traverse::new(self)
|
|
}
|
|
|
|
// Finds the split in the given direction if it exists
|
|
pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option<ViewId> {
|
|
let parent = self.nodes[id].parent;
|
|
// Base case, we found the root of the tree
|
|
if parent == id {
|
|
return None;
|
|
}
|
|
// Parent must always be a container
|
|
let parent_container = match &self.nodes[parent].content {
|
|
Content::Container(container) => container,
|
|
Content::View(_) => unreachable!(),
|
|
};
|
|
|
|
match (direction, parent_container.layout) {
|
|
(Direction::Up, Layout::Vertical)
|
|
| (Direction::Left, Layout::Horizontal)
|
|
| (Direction::Right, Layout::Horizontal)
|
|
| (Direction::Down, Layout::Vertical) => {
|
|
// The desired direction of movement is not possible within
|
|
// the parent container so the search must continue closer to
|
|
// the root of the split tree.
|
|
self.find_split_in_direction(parent, direction)
|
|
}
|
|
(Direction::Up, Layout::Horizontal)
|
|
| (Direction::Down, Layout::Horizontal)
|
|
| (Direction::Left, Layout::Vertical)
|
|
| (Direction::Right, Layout::Vertical) => {
|
|
// It's possible to move in the desired direction within
|
|
// the parent container so an attempt is made to find the
|
|
// correct child.
|
|
match self.find_child(id, &parent_container.children, direction) {
|
|
// Child is found, search is ended
|
|
Some(id) => Some(id),
|
|
// A child is not found. This could be because of either two scenarios
|
|
// 1. Its not possible to move in the desired direction, and search should end
|
|
// 2. A layout like the following with focus at X and desired direction Right
|
|
// | _ | x | |
|
|
// | _ _ _ | |
|
|
// | _ _ _ | |
|
|
// The container containing X ends at X so no rightward movement is possible
|
|
// however there still exists another view/container to the right that hasn't
|
|
// been explored. Thus another search is done here in the parent container
|
|
// before concluding it's not possible to move in the desired direction.
|
|
None => self.find_split_in_direction(parent, direction),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option<ViewId> {
|
|
let mut child_id = match direction {
|
|
// index wise in the child list the Up and Left represents a -1
|
|
// thus reversed iterator.
|
|
Direction::Up | Direction::Left => children
|
|
.iter()
|
|
.rev()
|
|
.skip_while(|i| **i != id)
|
|
.copied()
|
|
.nth(1)?,
|
|
// Down and Right => +1 index wise in the child list
|
|
Direction::Down | Direction::Right => {
|
|
children.iter().skip_while(|i| **i != id).copied().nth(1)?
|
|
}
|
|
};
|
|
let (current_x, current_y) = match &self.nodes[self.focus].content {
|
|
Content::View(current_view) => (current_view.area.left(), current_view.area.top()),
|
|
Content::Container(_) => unreachable!(),
|
|
};
|
|
|
|
// If the child is a container the search finds the closest container child
|
|
// visually based on screen location.
|
|
while let Content::Container(container) = &self.nodes[child_id].content {
|
|
match (direction, container.layout) {
|
|
(_, Layout::Vertical) => {
|
|
// find closest split based on x because y is irrelevant
|
|
// in a vertical container (and already correct based on previous search)
|
|
child_id = *container.children.iter().min_by_key(|id| {
|
|
let x = match &self.nodes[**id].content {
|
|
Content::View(view) => view.area.left(),
|
|
Content::Container(container) => container.area.left(),
|
|
};
|
|
(current_x as i16 - x as i16).abs()
|
|
})?;
|
|
}
|
|
(_, Layout::Horizontal) => {
|
|
// find closest split based on y because x is irrelevant
|
|
// in a horizontal container (and already correct based on previous search)
|
|
child_id = *container.children.iter().min_by_key(|id| {
|
|
let y = match &self.nodes[**id].content {
|
|
Content::View(view) => view.area.top(),
|
|
Content::Container(container) => container.area.top(),
|
|
};
|
|
(current_y as i16 - y as i16).abs()
|
|
})?;
|
|
}
|
|
}
|
|
}
|
|
Some(child_id)
|
|
}
|
|
|
|
pub fn prev(&self) -> ViewId {
|
|
// This function is very dumb, but that's because we don't store any parent links.
|
|
// (we'd be able to go parent.prev_sibling() recursively until we find something)
|
|
// For now that's okay though, since it's unlikely you'll be able to open a large enough
|
|
// number of splits to notice.
|
|
|
|
let mut views = self
|
|
.traverse()
|
|
.rev()
|
|
.skip_while(|&(id, _view)| id != self.focus)
|
|
.skip(1); // Skip focused value
|
|
if let Some((id, _)) = views.next() {
|
|
id
|
|
} else {
|
|
// extremely crude, take the last item
|
|
let (key, _) = self.traverse().next_back().unwrap();
|
|
key
|
|
}
|
|
}
|
|
|
|
pub fn next(&self) -> ViewId {
|
|
// This function is very dumb, but that's because we don't store any parent links.
|
|
// (we'd be able to go parent.next_sibling() recursively until we find something)
|
|
// For now that's okay though, since it's unlikely you'll be able to open a large enough
|
|
// number of splits to notice.
|
|
|
|
let mut views = self
|
|
.traverse()
|
|
.skip_while(|&(id, _view)| id != self.focus)
|
|
.skip(1); // Skip focused value
|
|
if let Some((id, _)) = views.next() {
|
|
id
|
|
} else {
|
|
// extremely crude, take the first item again
|
|
let (key, _) = self.traverse().next().unwrap();
|
|
key
|
|
}
|
|
}
|
|
|
|
pub fn transpose(&mut self) {
|
|
let focus = self.focus;
|
|
let parent = self.nodes[focus].parent;
|
|
if let Content::Container(container) = &mut self.nodes[parent].content {
|
|
container.layout = match container.layout {
|
|
Layout::Vertical => Layout::Horizontal,
|
|
Layout::Horizontal => Layout::Vertical,
|
|
};
|
|
self.recalculate();
|
|
}
|
|
}
|
|
|
|
pub fn swap_split_in_direction(&mut self, direction: Direction) -> Option<()> {
|
|
let focus = self.focus;
|
|
let target = self.find_split_in_direction(focus, direction)?;
|
|
let focus_parent = self.nodes[focus].parent;
|
|
let target_parent = self.nodes[target].parent;
|
|
|
|
if focus_parent == target_parent {
|
|
let parent = focus_parent;
|
|
let [parent, focus, target] = self.nodes.get_disjoint_mut([parent, focus, target])?;
|
|
match (&mut parent.content, &mut focus.content, &mut target.content) {
|
|
(
|
|
Content::Container(parent),
|
|
Content::View(focus_view),
|
|
Content::View(target_view),
|
|
) => {
|
|
let focus_pos = parent.children.iter().position(|id| focus_view.id == *id)?;
|
|
let target_pos = parent
|
|
.children
|
|
.iter()
|
|
.position(|id| target_view.id == *id)?;
|
|
// swap node positions so that traversal order is kept
|
|
parent.children[focus_pos] = target_view.id;
|
|
parent.children[target_pos] = focus_view.id;
|
|
// swap area so that views rendered at the correct location
|
|
std::mem::swap(&mut focus_view.area, &mut target_view.area);
|
|
|
|
Some(())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
} else {
|
|
let [focus_parent, target_parent, focus, target] =
|
|
self.nodes
|
|
.get_disjoint_mut([focus_parent, target_parent, focus, target])?;
|
|
match (
|
|
&mut focus_parent.content,
|
|
&mut target_parent.content,
|
|
&mut focus.content,
|
|
&mut target.content,
|
|
) {
|
|
(
|
|
Content::Container(focus_parent),
|
|
Content::Container(target_parent),
|
|
Content::View(focus_view),
|
|
Content::View(target_view),
|
|
) => {
|
|
let focus_pos = focus_parent
|
|
.children
|
|
.iter()
|
|
.position(|id| focus_view.id == *id)?;
|
|
let target_pos = target_parent
|
|
.children
|
|
.iter()
|
|
.position(|id| target_view.id == *id)?;
|
|
// re-parent target and focus nodes
|
|
std::mem::swap(
|
|
&mut focus_parent.children[focus_pos],
|
|
&mut target_parent.children[target_pos],
|
|
);
|
|
std::mem::swap(&mut focus.parent, &mut target.parent);
|
|
// swap area so that views rendered at the correct location
|
|
std::mem::swap(&mut focus_view.area, &mut target_view.area);
|
|
|
|
Some(())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn area(&self) -> Rect {
|
|
self.area
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Traverse<'a> {
|
|
tree: &'a Tree,
|
|
stack: Vec<ViewId>, // TODO: reuse the one we use on update
|
|
}
|
|
|
|
impl<'a> Traverse<'a> {
|
|
fn new(tree: &'a Tree) -> Self {
|
|
Self {
|
|
tree,
|
|
stack: vec![tree.root],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for Traverse<'a> {
|
|
type Item = (ViewId, &'a View);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
loop {
|
|
let key = self.stack.pop()?;
|
|
|
|
let node = &self.tree.nodes[key];
|
|
|
|
match &node.content {
|
|
Content::View(view) => return Some((key, view)),
|
|
Content::Container(container) => {
|
|
self.stack.extend(container.children.iter().rev());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DoubleEndedIterator for Traverse<'_> {
|
|
fn next_back(&mut self) -> Option<Self::Item> {
|
|
loop {
|
|
let key = self.stack.pop()?;
|
|
|
|
let node = &self.tree.nodes[key];
|
|
|
|
match &node.content {
|
|
Content::View(view) => return Some((key, view)),
|
|
Content::Container(container) => {
|
|
self.stack.extend(container.children.iter());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::editor::GutterConfig;
|
|
use crate::DocumentId;
|
|
|
|
#[test]
|
|
fn find_split_in_direction() {
|
|
let mut tree = Tree::new(Rect {
|
|
x: 0,
|
|
y: 0,
|
|
width: 180,
|
|
height: 80,
|
|
});
|
|
let mut view = View::new(DocumentId::default(), GutterConfig::default());
|
|
view.area = Rect::new(0, 0, 180, 80);
|
|
tree.insert(view);
|
|
|
|
let l0 = tree.focus;
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
let r0 = tree.focus;
|
|
|
|
tree.focus = l0;
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Horizontal);
|
|
let l1 = tree.focus;
|
|
|
|
tree.focus = l0;
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
|
|
// Tree in test
|
|
// | L0 | L2 | |
|
|
// | L1 | R0 |
|
|
let l2 = tree.focus;
|
|
assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left));
|
|
assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down));
|
|
assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right));
|
|
assert_eq!(None, tree.find_split_in_direction(l2, Direction::Up));
|
|
|
|
tree.focus = l1;
|
|
assert_eq!(None, tree.find_split_in_direction(l1, Direction::Left));
|
|
assert_eq!(None, tree.find_split_in_direction(l1, Direction::Down));
|
|
assert_eq!(Some(r0), tree.find_split_in_direction(l1, Direction::Right));
|
|
assert_eq!(Some(l0), tree.find_split_in_direction(l1, Direction::Up));
|
|
|
|
tree.focus = l0;
|
|
assert_eq!(None, tree.find_split_in_direction(l0, Direction::Left));
|
|
assert_eq!(Some(l1), tree.find_split_in_direction(l0, Direction::Down));
|
|
assert_eq!(Some(l2), tree.find_split_in_direction(l0, Direction::Right));
|
|
assert_eq!(None, tree.find_split_in_direction(l0, Direction::Up));
|
|
|
|
tree.focus = r0;
|
|
assert_eq!(Some(l2), tree.find_split_in_direction(r0, Direction::Left));
|
|
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Down));
|
|
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right));
|
|
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up));
|
|
}
|
|
|
|
#[test]
|
|
fn swap_split_in_direction() {
|
|
let mut tree = Tree::new(Rect {
|
|
x: 0,
|
|
y: 0,
|
|
width: 180,
|
|
height: 80,
|
|
});
|
|
|
|
let doc_l0 = DocumentId::default();
|
|
let mut view = View::new(doc_l0, GutterConfig::default());
|
|
view.area = Rect::new(0, 0, 180, 80);
|
|
tree.insert(view);
|
|
|
|
let l0 = tree.focus;
|
|
|
|
let doc_r0 = DocumentId::default();
|
|
let view = View::new(doc_r0, GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
let r0 = tree.focus;
|
|
|
|
tree.focus = l0;
|
|
|
|
let doc_l1 = DocumentId::default();
|
|
let view = View::new(doc_l1, GutterConfig::default());
|
|
tree.split(view, Layout::Horizontal);
|
|
let l1 = tree.focus;
|
|
|
|
tree.focus = l0;
|
|
|
|
let doc_l2 = DocumentId::default();
|
|
let view = View::new(doc_l2, GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
let l2 = tree.focus;
|
|
|
|
// Views in test
|
|
// | L0 | L2 | |
|
|
// | L1 | R0 |
|
|
|
|
// Document IDs in test
|
|
// | l0 | l2 | |
|
|
// | l1 | r0 |
|
|
|
|
fn doc_id(tree: &Tree, view_id: ViewId) -> Option<DocumentId> {
|
|
if let Content::View(view) = &tree.nodes[view_id].content {
|
|
Some(view.doc)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
tree.focus = l0;
|
|
// `*` marks the view in focus from view table (here L0)
|
|
// | l0* | l2 | |
|
|
// | l1 | r0 |
|
|
tree.swap_split_in_direction(Direction::Down);
|
|
// | l1 | l2 | |
|
|
// | l0* | r0 |
|
|
assert_eq!(tree.focus, l0);
|
|
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
|
|
assert_eq!(doc_id(&tree, l1), Some(doc_l0));
|
|
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
|
|
assert_eq!(doc_id(&tree, r0), Some(doc_r0));
|
|
|
|
tree.swap_split_in_direction(Direction::Right);
|
|
|
|
// | l1 | l2 | |
|
|
// | r0 | l0* |
|
|
assert_eq!(tree.focus, l0);
|
|
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
|
|
assert_eq!(doc_id(&tree, l1), Some(doc_r0));
|
|
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
|
|
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
|
|
|
|
// cannot swap, nothing changes
|
|
tree.swap_split_in_direction(Direction::Up);
|
|
// | l1 | l2 | |
|
|
// | r0 | l0* |
|
|
assert_eq!(tree.focus, l0);
|
|
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
|
|
assert_eq!(doc_id(&tree, l1), Some(doc_r0));
|
|
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
|
|
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
|
|
|
|
// cannot swap, nothing changes
|
|
tree.swap_split_in_direction(Direction::Down);
|
|
// | l1 | l2 | |
|
|
// | r0 | l0* |
|
|
assert_eq!(tree.focus, l0);
|
|
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
|
|
assert_eq!(doc_id(&tree, l1), Some(doc_r0));
|
|
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
|
|
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
|
|
|
|
tree.focus = l2;
|
|
// | l1 | l2* | |
|
|
// | r0 | l0 |
|
|
|
|
tree.swap_split_in_direction(Direction::Down);
|
|
// | l1 | r0 | |
|
|
// | l2* | l0 |
|
|
assert_eq!(tree.focus, l2);
|
|
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
|
|
assert_eq!(doc_id(&tree, l1), Some(doc_l2));
|
|
assert_eq!(doc_id(&tree, l2), Some(doc_r0));
|
|
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
|
|
|
|
tree.swap_split_in_direction(Direction::Up);
|
|
// | l2* | r0 | |
|
|
// | l1 | l0 |
|
|
assert_eq!(tree.focus, l2);
|
|
assert_eq!(doc_id(&tree, l0), Some(doc_l2));
|
|
assert_eq!(doc_id(&tree, l1), Some(doc_l1));
|
|
assert_eq!(doc_id(&tree, l2), Some(doc_r0));
|
|
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
|
|
}
|
|
|
|
#[test]
|
|
fn all_vertical_views_have_same_width() {
|
|
let tree_area_width = 180;
|
|
let mut tree = Tree::new(Rect {
|
|
x: 0,
|
|
y: 0,
|
|
width: tree_area_width,
|
|
height: 80,
|
|
});
|
|
let mut view = View::new(DocumentId::default(), GutterConfig::default());
|
|
view.area = Rect::new(0, 0, 180, 80);
|
|
tree.insert(view);
|
|
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Horizontal);
|
|
|
|
tree.remove(tree.focus);
|
|
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
|
|
// Make sure that we only have one level in the tree.
|
|
assert_eq!(3, tree.views().count());
|
|
assert_eq!(
|
|
vec![
|
|
tree_area_width / 3 - 1, // gap here
|
|
tree_area_width / 3 - 1, // gap here
|
|
tree_area_width / 3
|
|
],
|
|
tree.views()
|
|
.map(|(view, _)| view.area.width)
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vsplit_gap_rounding() {
|
|
let (tree_area_width, tree_area_height) = (80, 24);
|
|
let mut tree = Tree::new(Rect {
|
|
x: 0,
|
|
y: 0,
|
|
width: tree_area_width,
|
|
height: tree_area_height,
|
|
});
|
|
let mut view = View::new(DocumentId::default(), GutterConfig::default());
|
|
view.area = Rect::new(0, 0, tree_area_width, tree_area_height);
|
|
tree.insert(view);
|
|
|
|
for _ in 0..9 {
|
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
|
tree.split(view, Layout::Vertical);
|
|
}
|
|
|
|
assert_eq!(10, tree.views().count());
|
|
assert_eq!(
|
|
std::iter::repeat(7)
|
|
.take(9)
|
|
.chain(Some(8)) // Rounding in `recalculate`.
|
|
.collect::<Vec<_>>(),
|
|
tree.views()
|
|
.map(|(view, _)| view.area.width)
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
}
|