1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-27 12:23:46 +03:00

wezterm: add PaneNode tree to Tab

This expands the data model for Tab so that it can record a binary
tree of panes; this allows for conceptually splitting a Pane either
horizontally or vertically.

None of the other layers know about this concept yet.

refs: https://github.com/wez/wezterm/issues/157
This commit is contained in:
Wez Furlong 2020-06-27 08:39:03 -07:00
parent a6316315bb
commit 3d54a542bf
5 changed files with 470 additions and 12 deletions

View File

@ -141,7 +141,7 @@ impl Domain for LocalDomain {
let mux = Mux::get().unwrap();
let pane: Rc<dyn Pane> = Rc::new(LocalPane::new(terminal, child, pair.master, self.id));
let tab = Rc::new(Tab::new());
let tab = Rc::new(Tab::new(&size));
tab.assign_pane(&pane);
mux.add_tab(&tab)?;

View File

@ -92,36 +92,313 @@ pub struct SearchResult {
/// At this time only a single pane is supported
pub struct Tab {
id: TabId,
pane: RefCell<Option<Rc<dyn Pane>>>,
pane: RefCell<Option<PaneNode>>,
size: RefCell<PtySize>,
active: RefCell<usize>,
}
#[derive(Clone)]
pub struct PositionedPane {
/// The topological pane index that can be used to reference this pane
pub index: usize,
/// true if this is the active pane at the time the position was computed
pub is_active: bool,
/// The offset from the top left corner of the containing tab to the top
/// left corner of this pane, in cells.
pub left: usize,
/// The offset from the top left corner of the containing tab to the top
/// left corner of this pane, in cells.
pub top: usize,
/// The width of this pane in cells
pub width: usize,
/// The height of this pane in cells
pub height: usize,
/// The pane instance
pub pane: Rc<dyn Pane>,
}
/// A tab contains a tree of PaneNode's.
#[derive(Clone)]
enum PaneNode {
/// This node is filled with a single Pane
Single(Rc<dyn Pane>),
/// This node is split horizontally in two.
HorizontalSplit {
left: Box<PaneNode>,
left_width: usize,
right: Box<PaneNode>,
},
/// This node is split vertically in two.
VerticalSplit {
top: Box<PaneNode>,
top_height: usize,
bottom: Box<PaneNode>,
},
}
impl PaneNode {
/// Returns true if this node or any of its children are
/// alive. Stops evaluating as soon as it identifies that
/// something is alive.
pub fn is_alive(&self) -> bool {
match self {
PaneNode::Single(p) => !p.is_dead(),
PaneNode::HorizontalSplit { left, right, .. } => left.is_alive() || right.is_alive(),
PaneNode::VerticalSplit { top, bottom, .. } => top.is_alive() || bottom.is_alive(),
}
}
/// Returns a ref to the PaneNode::Single that contains a pane
/// given its topological index.
/// The if topological index is invalid, returns None.
fn node_by_index_mut(
&mut self,
wanted_index: usize,
current_index: &mut usize,
) -> Option<&mut Self> {
match self {
PaneNode::Single(_) => {
if wanted_index == *current_index {
Some(self)
} else {
*current_index += 1;
None
}
}
PaneNode::HorizontalSplit { left, right, .. } => {
if let Some(found) = left.node_by_index_mut(wanted_index, current_index) {
Some(found)
} else {
right.node_by_index_mut(wanted_index, current_index)
}
}
PaneNode::VerticalSplit { top, bottom, .. } => {
if let Some(found) = top.node_by_index_mut(wanted_index, current_index) {
Some(found)
} else {
bottom.node_by_index_mut(wanted_index, current_index)
}
}
}
}
/// Recursively Walk to compute the positioning information
fn walk(
&self,
active_index: usize,
x: usize,
y: usize,
width: usize,
height: usize,
panes: &mut Vec<PositionedPane>,
) {
match self {
PaneNode::Single(p) => {
let index = panes.len();
panes.push(PositionedPane {
index,
is_active: index == active_index,
left: x,
top: y,
width,
height,
pane: Rc::clone(p),
});
}
PaneNode::HorizontalSplit {
left,
left_width,
right,
} => {
left.walk(active_index, x, y, *left_width, height, panes);
right.walk(
active_index,
x + *left_width,
y,
width.saturating_sub(*left_width),
height,
panes,
);
}
PaneNode::VerticalSplit {
top,
top_height,
bottom,
} => {
top.walk(active_index, x, y, width, *top_height, panes);
bottom.walk(
active_index,
x,
y + *top_height,
width,
height.saturating_sub(*top_height),
panes,
);
}
}
}
}
impl Tab {
pub fn new() -> Self {
pub fn new(size: &PtySize) -> Self {
Self {
id: TAB_ID.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed),
pane: RefCell::new(None),
size: RefCell::new(*size),
active: RefCell::new(0),
}
}
/// Walks the pane tree to produce the topologically ordered flattened
/// list of PositionedPane instances along with their positioning information.
pub fn iter_panes(&self) -> Vec<PositionedPane> {
let mut panes = vec![];
let size = *self.size.borrow();
if let Some(pane) = self.pane.borrow().as_ref() {
pane.walk(
*self.active.borrow(),
0,
0,
size.cols as _,
size.rows as _,
&mut panes,
);
}
panes
}
pub fn tab_id(&self) -> TabId {
self.id
}
pub fn is_dead(&self) -> bool {
if let Some(pane) = self.get_active_pane() {
pane.is_dead()
if let Some(pane) = self.pane.borrow().as_ref() {
!pane.is_alive()
} else {
true
}
}
pub fn get_active_pane(&self) -> Option<Rc<dyn Pane>> {
self.pane.borrow().as_ref().map(Rc::clone)
self.iter_panes()
.iter()
.nth(*self.active.borrow())
.map(|p| Rc::clone(&p.pane))
}
/// Assigns the root pane.
/// This is suitable when creating a new tab and then assigning
/// the initial pane
pub fn assign_pane(&self, pane: &Rc<dyn Pane>) {
self.pane.borrow_mut().replace(Rc::clone(pane));
self.pane
.borrow_mut()
.replace(PaneNode::Single(Rc::clone(pane)));
}
fn cell_dimensions(&self) -> PtySize {
let size = *self.size.borrow();
PtySize {
rows: 1,
cols: 1,
pixel_width: size.pixel_width / size.cols,
pixel_height: size.pixel_height / size.rows,
}
}
/// Computes the size of the pane that would result if the specified
/// pane was split in a particular direction.
/// The intent is to call this prior to spawning the new pane so that
/// you can create it with the correct size.
/// May return None if the specified pane_index is invalid.
pub fn compute_split_size(
&self,
pane_index: usize,
direction: SplitDirection,
) -> Option<PtySize> {
let cell_dims = self.cell_dimensions();
self.iter_panes().iter().nth(pane_index).map(|pos| {
let (width, height) = match direction {
SplitDirection::Horizontal => (pos.width / 2, pos.height),
SplitDirection::Vertical => (pos.width, pos.height / 2),
};
PtySize {
rows: height as _,
cols: width as _,
pixel_width: cell_dims.pixel_width * width as u16,
pixel_height: cell_dims.pixel_height * height as u16,
}
})
}
/// Split the pane that has pane_index in the given direction and assign
/// the right/bottom pane of the newly created split to the provided Pane
/// instance. Returns the resultant index of the newly inserted pane.
/// Both the split and the inserted pane will be resized.
pub fn split_and_insert(
&self,
pane_index: usize,
direction: SplitDirection,
pane: Rc<dyn Pane>,
) -> anyhow::Result<usize> {
let new_size = self
.compute_split_size(pane_index, direction)
.ok_or_else(|| anyhow::anyhow!("invalid pane_index {}; cannot split!", pane_index))?;
pane.resize(new_size.clone())?;
let new_pane = Box::new(PaneNode::Single(pane));
if let Some(root_node) = self.pane.borrow_mut().as_mut() {
let mut active = 0;
let node = root_node
.node_by_index_mut(pane_index, &mut active)
.ok_or_else(|| {
anyhow::anyhow!("invalid pane_index {}; cannot split!", pane_index)
})?;
let prior_node = match node {
PaneNode::Single(orig_pane) => {
orig_pane.resize(new_size)?;
Box::new(PaneNode::Single(Rc::clone(orig_pane)))
}
_ => unreachable!("impossible PaneNode variant returned from node_by_index_mut"),
};
match direction {
SplitDirection::Horizontal => {
*node = PaneNode::HorizontalSplit {
left: prior_node,
right: new_pane,
left_width: new_size.cols as _,
}
}
SplitDirection::Vertical => {
*node = PaneNode::VerticalSplit {
top: prior_node,
bottom: new_pane,
top_height: new_size.rows as _,
}
}
}
let new_index = pane_index + 1;
*self.active.borrow_mut() = new_index;
Ok(new_index)
} else {
anyhow::bail!("no panes have been assigned; cannot split!");
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SplitDirection {
Horizontal,
Vertical,
}
/// A Pane represents a view on a terminal
@ -182,3 +459,179 @@ pub trait Pane: Downcast {
}
}
impl_downcast!(Pane);
#[cfg(test)]
mod test {
use super::*;
struct FakePane {
id: PaneId,
size: RefCell<PtySize>,
}
impl FakePane {
fn new(id: PaneId, size: PtySize) -> Rc<dyn Pane> {
Rc::new(Self {
id,
size: RefCell::new(size),
})
}
}
impl Pane for FakePane {
fn pane_id(&self) -> PaneId {
self.id
}
fn renderer(&self) -> RefMut<dyn Renderable> {
unimplemented!()
}
fn get_title(&self) -> String {
unimplemented!()
}
fn send_paste(&self, _text: &str) -> anyhow::Result<()> {
unimplemented!()
}
fn reader(&self) -> anyhow::Result<Box<dyn std::io::Read + Send>> {
unimplemented!()
}
fn writer(&self) -> RefMut<dyn std::io::Write> {
unimplemented!()
}
fn resize(&self, size: PtySize) -> anyhow::Result<()> {
*self.size.borrow_mut() = size;
Ok(())
}
fn key_down(&self, _key: KeyCode, _mods: KeyModifiers) -> anyhow::Result<()> {
unimplemented!()
}
fn mouse_event(&self, _event: MouseEvent) -> anyhow::Result<()> {
unimplemented!()
}
fn advance_bytes(&self, _buf: &[u8]) {
unimplemented!()
}
fn is_dead(&self) -> bool {
false
}
fn palette(&self) -> ColorPalette {
unimplemented!()
}
fn domain_id(&self) -> DomainId {
1
}
fn is_mouse_grabbed(&self) -> bool {
false
}
fn get_current_working_dir(&self) -> Option<Url> {
None
}
}
#[test]
fn tab_splitting() {
let size = PtySize {
rows: 24,
cols: 80,
pixel_width: 800,
pixel_height: 600,
};
let tab = Tab::new(&size);
tab.assign_pane(&FakePane::new(1, size));
let panes = tab.iter_panes();
assert_eq!(1, panes.len());
assert_eq!(0, panes[0].index);
assert_eq!(true, panes[0].is_active);
assert_eq!(0, panes[0].left);
assert_eq!(0, panes[0].top);
assert_eq!(80, panes[0].width);
assert_eq!(24, panes[0].height);
assert!(tab
.compute_split_size(1, SplitDirection::Horizontal)
.is_none());
let horz_size = tab
.compute_split_size(0, SplitDirection::Horizontal)
.unwrap();
assert_eq!(
horz_size,
PtySize {
rows: 24,
cols: 40,
pixel_width: 400,
pixel_height: 600
}
);
let vert_size = tab.compute_split_size(0, SplitDirection::Vertical).unwrap();
assert_eq!(
vert_size,
PtySize {
rows: 12,
cols: 80,
pixel_width: 800,
pixel_height: 300
}
);
let new_index = tab
.split_and_insert(0, SplitDirection::Horizontal, FakePane::new(2, horz_size))
.unwrap();
assert_eq!(new_index, 1);
let panes = tab.iter_panes();
assert_eq!(2, panes.len());
assert_eq!(0, panes[0].index);
assert_eq!(false, panes[0].is_active);
assert_eq!(0, panes[0].left);
assert_eq!(0, panes[0].top);
assert_eq!(40, panes[0].width);
assert_eq!(24, panes[0].height);
assert_eq!(1, panes[0].pane.pane_id());
assert_eq!(1, panes[1].index);
assert_eq!(true, panes[1].is_active);
assert_eq!(40, panes[1].left);
assert_eq!(0, panes[1].top);
assert_eq!(40, panes[1].width);
assert_eq!(24, panes[1].height);
assert_eq!(2, panes[1].pane.pane_id());
let vert_size = tab.compute_split_size(0, SplitDirection::Vertical).unwrap();
let new_index = tab
.split_and_insert(0, SplitDirection::Vertical, FakePane::new(3, vert_size))
.unwrap();
assert_eq!(new_index, 1);
let panes = tab.iter_panes();
assert_eq!(3, panes.len());
assert_eq!(0, panes[0].index);
assert_eq!(false, panes[0].is_active);
assert_eq!(0, panes[0].left);
assert_eq!(0, panes[0].top);
assert_eq!(40, panes[0].width);
assert_eq!(12, panes[0].height);
assert_eq!(1, panes[0].pane.pane_id());
assert_eq!(1, panes[1].index);
assert_eq!(true, panes[1].is_active);
assert_eq!(0, panes[1].left);
assert_eq!(12, panes[1].top);
assert_eq!(40, panes[1].width);
assert_eq!(12, panes[1].height);
assert_eq!(3, panes[1].pane.pane_id());
assert_eq!(2, panes[2].index);
assert_eq!(false, panes[2].is_active);
assert_eq!(40, panes[2].left);
assert_eq!(0, panes[2].top);
assert_eq!(40, panes[2].width);
assert_eq!(24, panes[2].height);
assert_eq!(2, panes[2].pane.pane_id());
}
}

View File

@ -241,12 +241,12 @@ impl ClientDomain {
// removed it from the mux. Let's add it back, but
// with a new id.
inner.remove_old_tab_mapping(entry.tab_id);
tab = Rc::new(Tab::new());
tab = Rc::new(Tab::new(&entry.size));
inner.record_remote_to_local_tab_mapping(entry.tab_id, tab.tab_id());
}
};
} else {
tab = Rc::new(Tab::new());
tab = Rc::new(Tab::new(&entry.size));
inner.record_remote_to_local_tab_mapping(entry.tab_id, tab.tab_id());
}
@ -372,7 +372,7 @@ impl Domain for ClientDomain {
result.tab_id
};
let pane: Rc<dyn Pane> = Rc::new(ClientPane::new(&inner, remote_tab_id, size, "wezterm"));
let tab = Rc::new(Tab::new());
let tab = Rc::new(Tab::new(&size));
tab.assign_pane(&pane);
let mux = Mux::get().unwrap();

View File

@ -265,7 +265,7 @@ impl Domain for RemoteSshDomain {
let mux = Mux::get().unwrap();
let pane: Rc<dyn Pane> = Rc::new(LocalPane::new(terminal, child, pair.master, self.id));
let tab = Rc::new(Tab::new());
let tab = Rc::new(Tab::new(&size));
tab.assign_pane(&pane);
mux.add_tab(&tab)?;

View File

@ -449,7 +449,12 @@ pub async fn run<
let pane = TermWizTerminalPane::new(domain.domain_id(), width, height, input_tx, render_rx);
let pane: Rc<dyn Pane> = Rc::new(pane);
let tab = Rc::new(Tab::new());
let tab = Rc::new(Tab::new(&PtySize {
rows: height as _,
cols: width as _,
pixel_width: 0,
pixel_height: 0,
}));
tab.assign_pane(&pane);
mux.add_tab(&tab)?;