Render runtime split-screen layout.

This commit is contained in:
Condorra 2024-08-22 22:25:05 +10:00
parent a002ac1c69
commit 525f69296e
4 changed files with 389 additions and 16 deletions

View File

@ -1,6 +1,7 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use term_split::TermSplit;
use yew::prelude::*; use yew::prelude::*;
pub mod command_handler; pub mod command_handler;
@ -8,6 +9,7 @@ pub mod lineengine;
pub mod lua_state; pub mod lua_state;
pub mod parsing; pub mod parsing;
pub mod split_panel; pub mod split_panel;
pub mod term_split;
pub mod term_view; pub mod term_view;
use crate::lua_state::{install_lua_globals, LuaState}; use crate::lua_state::{install_lua_globals, LuaState};
use crate::split_panel::*; use crate::split_panel::*;
@ -18,6 +20,7 @@ pub struct GlobalState {
// No strong references allowed between each of these groups of state. // No strong references allowed between each of these groups of state.
frame_registry: RefCell<RegisteredTermFrames>, frame_registry: RefCell<RegisteredTermFrames>,
lua_engine: RefCell<LuaState>, lua_engine: RefCell<LuaState>,
term_splits: RefCell<TermSplit>,
} }
type GlobalCell = Rc<GlobalState>; type GlobalCell = Rc<GlobalState>;
@ -35,13 +38,16 @@ fn app() -> Html {
let global = use_memo((), |_| GlobalState { let global = use_memo((), |_| GlobalState {
frame_registry: RegisteredTermFrames::new().into(), frame_registry: RegisteredTermFrames::new().into(),
lua_engine: LuaState::setup().expect("Can create interpreter").into(), lua_engine: LuaState::setup().expect("Can create interpreter").into(),
term_splits: TermSplit::Term {
frame: TermFrame(1),
}
.into(),
}); });
install_lua_globals(&global).expect("Couldn't install Lua globals"); install_lua_globals(&global).expect("Couldn't install Lua globals");
html! { html! {
<div class="toplevel"> <div class="toplevel">
<SplitPanel direction={PanelDirection::Vertical} first={ html! { <TermView terminal={TermFrame(0)} global={global.clone()}/> }} <TermViewTree global={global.clone()}/>
second={ html! { <TermView terminal={TermFrame(1)} global={global.clone()}/> } }/>
</div> </div>
} }
} }

View File

@ -128,7 +128,7 @@ mod tests {
parse_commands(""), parse_commands(""),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec![""] arguments: vec![""].into()
}] }]
} }
); );
@ -136,7 +136,7 @@ mod tests {
parse_commands("north"), parse_commands("north"),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["north"] arguments: vec!["north"].into()
}] }]
} }
); );
@ -145,7 +145,7 @@ mod tests {
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
// This is deliberate, ensures we can reconstruct the input. // This is deliberate, ensures we can reconstruct the input.
arguments: vec!["north", ""] arguments: vec!["north", ""].into()
}] }]
} }
); );
@ -154,10 +154,10 @@ mod tests {
ParseResult { ParseResult {
commands: vec![ commands: vec![
ParsedCommand { ParsedCommand {
arguments: vec!["#blah", "{x = 1 + 2; y = 3}"] arguments: vec!["#blah", "{x = 1 + 2; y = 3}"].into()
}, },
ParsedCommand { ParsedCommand {
arguments: vec!["#home"] arguments: vec!["#home"].into()
} }
] ]
} }
@ -166,7 +166,7 @@ mod tests {
parse_commands("#blah {x = 1 + 2"), parse_commands("#blah {x = 1 + 2"),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["#blah", "{x = 1 + 2"] arguments: vec!["#blah", "{x = 1 + 2"].into()
},] },]
} }
); );
@ -174,7 +174,7 @@ mod tests {
parse_commands("#blah {x = 1} {y = 1}"), parse_commands("#blah {x = 1} {y = 1}"),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["#blah", "{x = 1}", "{y = 1}"] arguments: vec!["#blah", "{x = 1}", "{y = 1}"].into()
}] }]
} }
); );
@ -182,7 +182,7 @@ mod tests {
parse_commands("#blah \"hello\" \"world\""), parse_commands("#blah \"hello\" \"world\""),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["#blah", "\"hello\"", "\"world\""] arguments: vec!["#blah", "\"hello\"", "\"world\""].into()
}] }]
} }
); );
@ -190,7 +190,7 @@ mod tests {
parse_commands("#blah {x = \"}\"} {y = 1}"), parse_commands("#blah {x = \"}\"} {y = 1}"),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["#blah", "{x = \"}\"}", "{y = 1}"] arguments: vec!["#blah", "{x = \"}\"}", "{y = 1}"].into()
}] }]
} }
); );
@ -204,9 +204,10 @@ mod tests {
"{x = \"}\"; a = \"{\"; y = {}; z = 1;}", "{x = \"}\"; a = \"{\"; y = {}; z = 1;}",
"{ q = 5 }" "{ q = 5 }"
] ]
.into()
}, },
ParsedCommand { ParsedCommand {
arguments: vec![""] arguments: vec![""].into()
} }
] ]
} }
@ -215,7 +216,7 @@ mod tests {
parse_commands("#blah {\\}\\}\\}} {y = 1}"), parse_commands("#blah {\\}\\}\\}} {y = 1}"),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["#blah", "{\\}\\}\\}}", "{y = 1}"] arguments: vec!["#blah", "{\\}\\}\\}}", "{y = 1}"].into()
}] }]
} }
); );
@ -223,7 +224,7 @@ mod tests {
parse_commands("#blah \"This is a \\\"test\\\"\""), parse_commands("#blah \"This is a \\\"test\\\"\""),
ParseResult { ParseResult {
commands: vec![ParsedCommand { commands: vec![ParsedCommand {
arguments: vec!["#blah", "\"This is a \\\"test\\\"\""] arguments: vec!["#blah", "\"This is a \\\"test\\\"\""].into()
}] }]
} }
); );

332
src/term_split.rs Normal file
View File

@ -0,0 +1,332 @@
use itertools::Itertools;
use std::collections::BTreeMap;
use crate::TermFrame;
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum TermSplit {
Term {
frame: TermFrame,
},
Horizontal {
left: Box<TermSplit>,
right: Box<TermSplit>,
},
Vertical {
top: Box<TermSplit>,
bottom: Box<TermSplit>,
},
}
impl TermSplit {
fn collect_term_frames(&self, into: &mut BTreeMap<TermFrame, usize>) {
match self {
TermSplit::Term { frame } => {
into.entry(frame.clone())
.and_modify(|v| *v += 1)
.or_insert(1);
}
TermSplit::Horizontal { left, right } => {
left.collect_term_frames(into);
right.collect_term_frames(into);
}
TermSplit::Vertical { top, bottom } => {
top.collect_term_frames(into);
bottom.collect_term_frames(into);
}
}
}
pub fn validate(&self) -> Result<(), String> {
let mut frame_count: BTreeMap<TermFrame, usize> = BTreeMap::new();
self.collect_term_frames(&mut frame_count);
let duplicate_terminal = frame_count
.iter()
.filter_map(|(k, v)| {
if *v > 1 {
Some(format!("Terminal {}", k.0))
} else {
None
}
})
.join(", ");
if !duplicate_terminal.is_empty() {
Err(format!(
"Attempt to create layout that duplicates reference to: {}",
duplicate_terminal
))?;
}
Ok(())
}
pub fn modify_at_pathstr<F>(&mut self, pathstr: &str, mod_with: F) -> Result<(), String>
where
F: FnOnce(&mut TermSplit) -> Result<(), String>,
{
self.modify_at_pathstr_vec(&pathstr.chars().collect::<Vec<char>>(), mod_with)
}
fn modify_at_pathstr_vec<F>(&mut self, pathstr: &[char], mod_with: F) -> Result<(), String>
where
F: FnOnce(&mut TermSplit) -> Result<(), String>,
{
match self {
TermSplit::Horizontal { left, right } => match pathstr.split_first() {
None => mod_with(self),
Some(('l', path_rest)) => left.modify_at_pathstr_vec(path_rest, mod_with),
Some(('r', path_rest)) => right.modify_at_pathstr_vec(path_rest, mod_with),
Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a horizontal split", c, path_rest.iter().collect::<String>()))
},
TermSplit::Vertical { top, bottom } => match pathstr.split_first() {
None => mod_with(self),
Some(('t', path_rest)) => top.modify_at_pathstr_vec(path_rest, mod_with),
Some(('b', path_rest)) => bottom.modify_at_pathstr_vec(path_rest, mod_with),
Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::<String>()))
},
TermSplit::Term { .. } => match pathstr.split_first() {
None => mod_with(self),
Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::<String>()))
}
}
}
pub fn hsplit(&mut self, pathstr: &str, new_frame: TermFrame) -> Result<(), String> {
let mut new = self.clone();
new.modify_at_pathstr(pathstr, move |n| {
*n = TermSplit::Horizontal {
left: n.clone().into(),
right: TermSplit::Term { frame: new_frame }.into(),
};
Ok(())
})?;
new.validate()?;
*self = new;
Ok(())
}
pub fn vsplit(&mut self, pathstr: &str, new_frame: TermFrame) -> Result<(), String> {
let mut new = self.clone();
new.modify_at_pathstr(pathstr, move |n| {
*n = TermSplit::Vertical {
top: n.clone().into(),
bottom: TermSplit::Term { frame: new_frame }.into(),
};
Ok(())
})?;
new.validate()?;
*self = new;
Ok(())
}
pub fn join(&mut self, pathstr: &str) -> Result<(), String> {
self.modify_at_pathstr(pathstr, move |n| match n {
TermSplit::Term { .. } => {
Err("Can only join vertical or horizontal splits, not a terminal".to_owned())
}
TermSplit::Horizontal { left, .. } => {
*n = (**left).clone();
Ok(())
}
TermSplit::Vertical { top, .. } => {
*n = (**top).clone();
Ok(())
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_accepts_valid() {
use TermSplit::*;
assert_eq!(
(Vertical {
top: Term {
frame: TermFrame(1)
}
.into(),
bottom: Horizontal {
left: Term {
frame: TermFrame(2)
}
.into(),
right: Term {
frame: TermFrame(3)
}
.into(),
}
.into()
})
.validate(),
Ok(())
);
}
#[test]
fn validate_rejects_duplicate() {
use TermSplit::*;
assert_eq!(
(Vertical {
top: Term {
frame: TermFrame(1)
}
.into(),
bottom: Horizontal {
left: Term {
frame: TermFrame(1)
}
.into(),
right: Term {
frame: TermFrame(3)
}
.into(),
}
.into()
})
.validate(),
Err("Attempt to create layout that duplicates reference to: Terminal 1".to_owned())
);
assert_eq!(
(Vertical {
top: Term {
frame: TermFrame(42)
}
.into(),
bottom: Horizontal {
left: Term {
frame: TermFrame(1)
}
.into(),
right: Term {
frame: TermFrame(42)
}
.into(),
}
.into()
})
.validate(),
Err("Attempt to create layout that duplicates reference to: Terminal 42".to_owned())
);
}
#[test]
fn modify_at_pathstr_works() {
use TermSplit::*;
let mut t = Term {
frame: TermFrame(1),
};
assert_eq!(
t.modify_at_pathstr("", |v| {
*v = Term {
frame: TermFrame(2),
};
Ok(())
}),
Ok(())
);
assert_eq!(
t,
Term {
frame: TermFrame(2)
}
);
assert_eq!(
t.modify_at_pathstr("tlr", |v| {
*v = Term {
frame: TermFrame(2),
};
Ok(())
}),
Err("In split path, found trailing junk tlr after addressing terminal".to_owned())
);
let mut t = Vertical {
top: Horizontal {
left: Horizontal {
left: Term {
frame: TermFrame(42),
}
.into(),
right: Term {
frame: TermFrame(64),
}
.into(),
}
.into(),
right: Term {
frame: TermFrame(42),
}
.into(),
}
.into(),
bottom: Vertical {
top: Term {
frame: TermFrame(43),
}
.into(),
bottom: Term {
frame: TermFrame(44),
}
.into(),
}
.into(),
};
assert_eq!(
t.modify_at_pathstr("tlr", |v| {
*v = Term {
frame: TermFrame(2),
};
Ok(())
}),
Ok(())
);
assert_eq!(
t.modify_at_pathstr("bb", |v| {
*v = Term {
frame: TermFrame(3),
};
Ok(())
}),
Ok(())
);
assert_eq!(
t,
Vertical {
top: Horizontal {
left: Horizontal {
left: Term {
frame: TermFrame(42),
}
.into(),
right: Term {
frame: TermFrame(2),
}
.into(),
}
.into(),
right: Term {
frame: TermFrame(42),
}
.into(),
}
.into(),
bottom: Vertical {
top: Term {
frame: TermFrame(43),
}
.into(),
bottom: Term {
frame: TermFrame(3),
}
.into(),
}
.into(),
}
);
}
}

View File

@ -7,7 +7,8 @@ use yew::prelude::*;
use crate::{ use crate::{
command_handler::command_handler, command_handler::command_handler,
lineengine::line::{Readline, ReadlineEvent}, lineengine::line::{Readline, ReadlineEvent},
GlobalCell, term_split::TermSplit,
GlobalCell, PanelDirection, SplitPanel,
}; };
#[wasm_bindgen] #[wasm_bindgen]
@ -64,7 +65,7 @@ extern "C" {
fn fit(this: &FitAddon); fn fit(this: &FitAddon);
} }
#[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone)] #[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone, Debug)]
pub struct TermFrame(pub u64); pub struct TermFrame(pub u64);
#[derive(Properties)] #[derive(Properties)]
@ -212,6 +213,39 @@ pub fn term_view(props: &TermViewProps) -> Html {
} }
} }
#[derive(Properties, PartialEq)]
pub struct TermViewTreeProps {
pub global: GlobalCell,
}
#[function_component(TermViewTree)]
pub fn term_view_tree(props: &TermViewTreeProps) -> Html {
fn mk_term_view_tree(global: &GlobalCell, split: &TermSplit) -> Html {
use TermSplit::*;
match split {
Term { frame } => html! {
<TermView global={global.clone()} terminal={frame.clone()}/>
},
Horizontal { left, right } => html! {
<SplitPanel
direction={PanelDirection::Horizontal}
first={mk_term_view_tree(global, left)}
second={mk_term_view_tree(global, right)}
/>
},
Vertical { top, bottom } => html! {
<SplitPanel
direction={PanelDirection::Vertical}
first={mk_term_view_tree(global, top)}
second={mk_term_view_tree(global, bottom)}
/>
},
}
}
mk_term_view_tree(&props.global, &props.global.term_splits.borrow())
}
pub fn echo_to_term_frame( pub fn echo_to_term_frame(
global: &GlobalCell, global: &GlobalCell,
frame_id: &TermFrame, frame_id: &TermFrame,