use std::collections::HashMap; use gc_arena::Collect; use wasm_bindgen::prelude::*; use web_sys::{console, Element, Node}; use yew::prelude::*; use crate::{ command_handler::command_handler, editor_view::CodeEditorView, html_view::HtmlView, lineengine::line::{Readline, ReadlineEvent}, tab_panel::TabPanel, term_split::TermSplit, timer_host::TimerHost, GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel, }; #[wasm_bindgen] extern "C" { #[derive(PartialEq)] pub type Terminal; // Sadly, can't do type parameters with wasm_bindgen. pub type IEventString; pub type IEventDims; pub type IDisposable; #[wasm_bindgen(constructor)] fn new() -> Terminal; #[wasm_bindgen(method)] fn open(this: &Terminal, element: &Element); #[wasm_bindgen(method)] pub fn write(this: &Terminal, data: &str); // Todo: Can we do this with interfaces somehow? #[wasm_bindgen(method)] fn loadAddon(this: &Terminal, addon: &FitAddon); #[wasm_bindgen(method)] fn onData(this: &Terminal, listener: &Closure) -> IDisposable; #[wasm_bindgen(method)] fn onResize(this: &Terminal, listener: &Closure) -> IDisposable; #[wasm_bindgen(method, getter)] fn rows(this: &Terminal) -> u16; #[wasm_bindgen(method, getter)] fn cols(this: &Terminal) -> u16; pub type Dims; #[wasm_bindgen(method, getter)] fn rows(this: &Dims) -> u16; #[wasm_bindgen(method, getter)] fn cols(this: &Dims) -> u16; #[wasm_bindgen(method, setter)] fn set_listener(this: &IEventString, handler: &Closure) -> IDisposable; #[wasm_bindgen(method, setter)] fn set_listener(this: &IEventDims, handler: &Closure) -> IDisposable; #[wasm_bindgen(method)] fn dispose(this: &IDisposable); #[derive(PartialEq)] pub type FitAddon; } #[wasm_bindgen(js_namespace=FitAddon)] extern "C" { #[wasm_bindgen(constructor)] fn new() -> FitAddon; #[wasm_bindgen(method)] fn fit(this: &FitAddon); } #[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone, Debug, Collect)] #[collect(require_static)] pub struct FrameId(pub u64); #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)] pub enum FrameViewType { Terminal, Editor, Html { inner: String }, } #[derive(Properties)] pub struct FrameData { pub id: FrameId, pub term: Terminal, pub fit: FitAddon, pub node: Node, pub readline: Readline, pub timer_host: TimerHost, pub retained_closures: Option<( Closure, Closure, Closure JsValue>, )>, } impl PartialEq for FrameData { fn eq(&self, other: &Self) -> bool { // Only the ID matters, the rest are just reference data. self.id == other.id } } pub type RegisteredTermFrames = HashMap; pub(super) fn get_or_make_term_frame<'a>( frame: &FrameId, frames: &'a mut RegisteredTermFrames, // Only used for callbacks, expected frames already borrowed mut! globals: &GlobalMemoCell, ) -> &'a mut FrameData { frames.entry(frame.clone()).or_insert_with(|| { let term = Terminal::new(); let fit = FitAddon::new(); let element = web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.create_element("div").ok()) .expect("Can create element for term"); element.set_class_name("hterminal"); let fit_for_resize = FitAddon { obj: fit.clone() }; let resize_element_closure: Closure JsValue> = Closure::new(move |_: JsValue| { fit_for_resize.fit(); JsValue::TRUE }); web_sys::ResizeObserver::new(resize_element_closure.as_ref().unchecked_ref()) .expect("Couldn't construct ResizeObserver") .observe(&element); term.open(&element); term.loadAddon(&fit); fit.fit(); let term_for_readline: Terminal = Terminal { obj: term.clone() }; let initial_size = (term.cols(), term.rows()); let mut new_data: FrameData = FrameData { id: frame.clone(), term: Terminal { obj: term.clone() }, fit, node: element.into(), readline: Readline::new( "".to_owned(), Box::new(move |dat| { term_for_readline.write( std::str::from_utf8(dat).expect("readline tried to emit invalid UTF-8"), ) }), initial_size, ), timer_host: TimerHost::new(frame.clone()), retained_closures: None, }; let frame_for_resize: FrameId = frame.clone(); let globals_for_resize: GlobalMemoCell = globals.clone(); let resize_closure = Closure::new(move |dims: Dims| { globals_for_resize .frame_registry .try_borrow_mut() .ok() .and_then(|mut frame_reg| { frame_reg.get_mut(&frame_for_resize).and_then(|frame| { frame .readline .handle_resize((dims.cols(), dims.rows())) .ok() }) }) .unwrap_or(()); let mut engine = globals_for_resize.lua_engine.borrow_mut(); match engine.dispatch_resize(&frame_for_resize, dims.cols(), dims.rows()) { Err(e) => console::log_1(&JsValue::from_str(&format!( "Error sending resize to Lua: {}", e ))), Ok(()) => {} } }); term.onResize(&resize_closure); let frame_for_ondata: FrameId = frame.clone(); let globals_for_ondata: GlobalMemoCell = globals.clone(); let data_closure = Closure::new(move |d: String| { let new_lines: Vec = match globals_for_ondata .frame_registry .try_borrow_mut() .expect("Unexpected failure to borrow frame registry in onData") .get_mut(&frame_for_ondata) { None => vec![], Some(frame) => frame .readline .readline(d.as_bytes()) .expect("Readline failed") .into_iter() .filter_map(|ev| match ev { ReadlineEvent::Line(l) => Some(l), _ => None, }) .collect(), }; // We deliberately stop borrowing anything from RefCells here since command_handler has // lots of its own borrows, and we don't want them to conflict. for cmd in &new_lines { command_handler(&globals_for_ondata, &frame_for_ondata, cmd); } }); term.onData(&data_closure); new_data .retained_closures .replace((data_closure, resize_closure, resize_element_closure)); new_data }) } #[derive(Properties, PartialEq)] pub struct TermViewProps { pub terminal: FrameId, pub global_memo: GlobalMemoCell, pub global_layout: UseStateHandle, } #[function_component(TermView)] pub fn term_view(props: &TermViewProps) -> Html { let mut frame_reg = props.global_memo.frame_registry.borrow_mut(); let term = get_or_make_term_frame(&props.terminal, &mut frame_reg, &props.global_memo); term.fit.fit(); match props .global_layout .frame_views .get(&props.terminal) .cloned() .unwrap_or(FrameViewType::Terminal) { FrameViewType::Terminal => Html::VRef(term.node.clone()), FrameViewType::Editor => html! { }, FrameViewType::Html { inner } => html! { }, } } #[derive(Properties, PartialEq)] pub struct TermViewTreeProps { pub global_memo: GlobalMemoCell, pub global_layout: UseStateHandle, } #[function_component(TermViewTree)] pub fn term_view_tree(props: &TermViewTreeProps) -> Html { fn mk_term_view_tree( global: &GlobalMemoCell, layout: UseStateHandle, split: &TermSplit, ) -> Html { use TermSplit::*; match split { Term { frame } => html! { }, Horizontal { left, right } => html! { }, Vertical { top, bottom } => html! { }, Tabs { tabs } => html! { >()} /> }, } } mk_term_view_tree( &props.global_memo, props.global_layout.clone(), &props.global_layout.term_splits, ) } pub fn echo_to_term_frame( global: &GlobalMemoCell, frame_id: &FrameId, message: &str, ) -> Result<(), &'static str> { global .frame_registry .borrow() .get(frame_id) .ok_or("Attempt to echo to frame that doesn't exist.")? .term .write(message); Ok(()) }