use std::{ops::Deref, rc::Rc}; use crate::{FrameId, GlobalLayoutState, GlobalMemoCell, PanelDirection, SplitPanel}; use anyhow::Error; use editor_area::EditorArea; use itertools::Itertools; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{window, HtmlInputElement, KeyboardEvent}; use yew::{ function_component, html, use_effect_with, use_node_ref, use_state, use_state_eq, AttrValue, Callback, Html, Properties, UseStateHandle, }; mod editor_area; #[derive(Properties, PartialEq, Clone)] pub struct EditorViewProps { pub frame: FrameId, pub global_memo: GlobalMemoCell, pub global_layout: UseStateHandle>, } #[derive(Properties, PartialEq, Clone)] pub struct EditorViewDetailProps { pub frame: FrameId, pub global_memo: GlobalMemoCell, pub global_layout: UseStateHandle>, pub editor_state: UseStateHandle>, } fn close_editor(frame: &FrameId, global_layout: &UseStateHandle>) { let mut gl_new: GlobalLayoutState = global_layout.as_ref().clone(); gl_new.frame_views.remove(frame); global_layout.set(gl_new.into()); } #[derive(Clone, PartialEq)] pub struct EditorViewState { error_msg: Option, available_files: Vec, open_file: AttrValue, show_create_dialog: bool, show_delete_dialog: Option, } fn fetch_initial_editor_state_or_fail() -> anyhow::Result { let win = window().ok_or_else(|| Error::msg("Can't get window"))?; let local = win .local_storage() .map_err(|_| Error::msg("Error retrieving localStorage"))? .ok_or_else(|| Error::msg("Local storage not available"))?; let n_keys = local .length() .map_err(|_| Error::msg("localStorage broken"))?; let mut available_files: Vec = vec![]; for i in 0..n_keys { if let Some(key) = local .key(i) .map_err(|_| Error::msg("localStorage broken"))? { match key.strip_prefix("scriptfile_") { None => {} Some(filename) => { available_files.push(filename.to_owned().into()); } } } } let init_str: AttrValue = "init.lua".into(); if !available_files.contains(&init_str) { available_files.push(init_str); local .set( "scriptfile_init.lua", "-- Put any code to run on every client load here.\n", ) .map_err(|_| Error::msg("localStorage broken"))?; } Ok(EditorViewState { error_msg: None, available_files, open_file: "init.lua".into(), show_create_dialog: false, show_delete_dialog: None, }) } fn fetch_initial_editor_state() -> Rc { match fetch_initial_editor_state_or_fail() { Ok(s) => s.into(), Err(e) => EditorViewState { error_msg: Some(format!("Can't load editor data: {}", e).into()), available_files: vec![], open_file: "init.lua".into(), show_create_dialog: false, show_delete_dialog: None, } .into(), } } fn load_file_contents(file: &str) -> anyhow::Result { let win = window().ok_or_else(|| Error::msg("Can't get window"))?; let local = win .local_storage() .map_err(|_| Error::msg("Error retrieving localStorage"))? .ok_or_else(|| Error::msg("Local storage not available"))?; Ok(local .get(&format!("scriptfile_{}", file)) .map_err(|_| Error::msg("Error retrieving localStorage"))? .unwrap_or_else(|| "".to_owned())) } pub fn try_run_script(script: &str, global_memo: &GlobalMemoCell) -> anyhow::Result<()> { let script = load_file_contents(script)?; global_memo .lua_engine .borrow_mut() .execute(&script) .map_err(Error::msg)?; Ok(()) } fn run_script(script: &str, global_memo: &GlobalMemoCell, set_err: Rc) where F: Fn(Option<&str>), { match try_run_script(script, global_memo) { Ok(()) => { set_err(None); } Err(e) => { set_err(Some(&format!("Script error: {}", e))); } } } fn show_create_dialog(state: &UseStateHandle>) { let mut new_state: EditorViewState = (*state.deref().deref()).clone(); new_state.show_create_dialog = true; state.set(new_state.into()); } #[function_component(EditorNav)] fn editor_nav(props: &EditorViewDetailProps) -> Html { let global_memo = props.global_memo.clone(); let global_layout = props.global_layout.clone(); let frame = props.frame.clone(); let current_script = props.editor_state.open_file.clone(); let set_err_state = props.editor_state.clone(); let set_err = Rc::new(move |msg: Option<&str>| { let mut new_state = (*set_err_state.as_ref()).clone(); new_state.error_msg = msg.map(|v| AttrValue::from(String::from(v))); set_err_state.set(new_state.into()); }); let editor_state = props.editor_state.clone(); html! {
    {props .editor_state .available_files .iter() .map(|f| { let mut classes = vec!["list-group-item", "pe-auto"]; let mut aria_current = None; if *f == props.editor_state.open_file { aria_current = Some("true"); classes.push("active"); } html! {
  • {f}
  • } }) .collect::>() }
} } #[derive(Properties, PartialEq, Clone)] pub struct ErrorBarProps { pub msg: AttrValue, pub dismiss: Callback<()>, } #[function_component(ErrorBar)] fn error_bar(props: &ErrorBarProps) -> Html { let props = props.clone(); html! { } } fn close_modals(state: &UseStateHandle>) { let mut new_state = (*state.deref().deref()).clone(); new_state.show_create_dialog = false; new_state.show_delete_dialog = None; state.set(new_state.into()); } fn create_script(state: &UseStateHandle>) { let mut scriptname = window() .and_then(|w| w.document()) .and_then(|d| d.get_element_by_id("newscriptname")) .map(|el| el.unchecked_into::().value()) .unwrap_or_default(); if scriptname.is_empty() { scriptname = "untitled.lua".to_owned(); } let mut new_state = (*state.deref().deref()).clone(); let scriptname: AttrValue = scriptname.into(); if new_state.available_files.contains(&scriptname) { new_state.error_msg = Some("A script with that name already exists".into()); } new_state.available_files.push(scriptname.clone()); new_state.open_file = scriptname; new_state.show_create_dialog = false; state.set(new_state.into()); } #[function_component(CodeEditorView)] pub fn editor_view(props: &EditorViewProps) -> Html { let editor_state: UseStateHandle> = use_state_eq(fetch_initial_editor_state); let detail_props: EditorViewDetailProps = EditorViewDetailProps { frame: props.frame.clone(), global_memo: props.global_memo.clone(), global_layout: props.global_layout.clone(), editor_state: editor_state.clone(), }; let current_script = editor_state.open_file.clone(); let set_err_state = editor_state.clone(); let set_err = Rc::new(move |msg: Option<&str>| { let mut new_state = (*set_err_state.as_ref()).clone(); new_state.error_msg = msg.map(|v| AttrValue::from(String::from(v))); set_err_state.set(new_state.into()); }); let editor_ref = use_node_ref(); let global_layout = props.global_layout.clone(); let frame = props.frame.clone(); type KbClosure = Closure; let editor_closure: UseStateHandle>> = use_state(|| None); let editor_ref_eff = editor_ref.clone(); let global_memo = props.global_memo.clone(); use_effect_with(current_script.clone(), move |_| { if let Some(editor_node) = editor_ref_eff.get() { let closure = Closure::new(move |ev: KeyboardEvent| { if ev.code() == "Enter" && ev.get_modifier_state("Control") { run_script(¤t_script, &global_memo, set_err.clone()); ev.prevent_default(); } else if ev.code() == "Escape" { close_editor(&frame, &global_layout); } }); let _ = editor_node .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()); let closure: Rc> = closure.into(); editor_closure.set(Some(closure.clone())); let cleanup: Box = Box::new(move || { let _ = editor_node.remove_event_listener_with_callback( "keydown", (*closure.as_ref()).as_ref().unchecked_ref(), ); }); return cleanup; } Box::new(|| {}) }); let state_modalclose1 = editor_state.clone(); let state_modalclose2 = editor_state.clone(); let state_createscript = editor_state.clone(); html! {
{if editor_state.show_create_dialog { html! { <> } }