165 lines
5.0 KiB
Rust
165 lines
5.0 KiB
Rust
|
use std::{ops::Deref, rc::Rc};
|
||
|
|
||
|
use anyhow::Error;
|
||
|
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
|
||
|
use web_sys::{window, Node};
|
||
|
use yew::{
|
||
|
function_component, html, use_effect_with, use_node_ref, use_state_eq, AttrValue, Callback,
|
||
|
Html, Properties, UseStateHandle,
|
||
|
};
|
||
|
|
||
|
use crate::{FrameId, GlobalLayoutState, GlobalMemoCell, PanelDirection, SplitPanel};
|
||
|
|
||
|
#[derive(Properties, PartialEq, Clone)]
|
||
|
pub struct EditorViewProps {
|
||
|
pub frame: FrameId,
|
||
|
pub global_memo: GlobalMemoCell,
|
||
|
pub global_layout: UseStateHandle<Rc<GlobalLayoutState>>,
|
||
|
}
|
||
|
|
||
|
fn close_editor(frame: &FrameId, global_layout: &UseStateHandle<Rc<GlobalLayoutState>>) {
|
||
|
let mut gl_new: GlobalLayoutState = global_layout.as_ref().clone();
|
||
|
gl_new.frame_views.remove(frame);
|
||
|
global_layout.set(gl_new.into());
|
||
|
}
|
||
|
|
||
|
#[derive(PartialEq, Clone)]
|
||
|
pub struct EditorState {
|
||
|
error_msg: Option<AttrValue>,
|
||
|
available_files: Vec<AttrValue>,
|
||
|
}
|
||
|
|
||
|
fn fetch_initial_editor_state_or_fail() -> anyhow::Result<EditorState> {
|
||
|
let win = window().ok_or_else(|| Error::msg("Can't get window"))?;
|
||
|
win.local_storage()
|
||
|
.map_err(|_| Error::msg("Error retrieving localStorage"))?
|
||
|
.ok_or_else(|| Error::msg("Local storage not available"))?;
|
||
|
Ok(EditorState {
|
||
|
error_msg: None,
|
||
|
available_files: vec![],
|
||
|
})
|
||
|
}
|
||
|
|
||
|
fn fetch_initial_editor_state() -> EditorState {
|
||
|
match fetch_initial_editor_state_or_fail() {
|
||
|
Ok(s) => s,
|
||
|
Err(e) => EditorState {
|
||
|
error_msg: Some(format!("Can't load editor data: {}", e.to_string()).into()),
|
||
|
available_files: vec![],
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[function_component(EditorNav)]
|
||
|
fn editor_nav(props: &EditorViewProps) -> Html {
|
||
|
let global_layout = props.global_layout.clone();
|
||
|
let frame = props.frame.clone();
|
||
|
html! {
|
||
|
<div class="editornav">
|
||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||
|
<button class="btn" aria-label="New file" title="New file"><i class="bi bi-file-earmark-plus"></i></button>
|
||
|
<div class="flex-fill"/>
|
||
|
<button class="btn" onclick={move |_ev| close_editor(&frame, &global_layout)} aria-label="Close Editor" title="Close editor"><i class="bi bi-arrow-return-left"></i></button>
|
||
|
</nav>
|
||
|
</div>
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[wasm_bindgen(getter_with_clone)]
|
||
|
struct EditorViewConfig {
|
||
|
pub doc: String,
|
||
|
pub extensions: Vec<CMExtension>,
|
||
|
pub parent: Node,
|
||
|
}
|
||
|
|
||
|
#[wasm_bindgen(module = codemirror)]
|
||
|
extern "C" {
|
||
|
type EditorView;
|
||
|
#[derive(Clone)]
|
||
|
type CMExtension;
|
||
|
|
||
|
#[wasm_bindgen(constructor)]
|
||
|
fn new(settings: EditorViewConfig) -> EditorView;
|
||
|
|
||
|
#[wasm_bindgen(js_name = basicSetup, thread_local)]
|
||
|
static BASIC_SETUP: CMExtension;
|
||
|
}
|
||
|
|
||
|
#[wasm_bindgen(module = "@codemirror/legacy-modes/mode/lua")]
|
||
|
extern "C" {
|
||
|
#[wasm_bindgen(js_name = lua, thread_local)]
|
||
|
static LUA: StreamLanguage;
|
||
|
}
|
||
|
|
||
|
#[wasm_bindgen(module = "@codemirror/language")]
|
||
|
extern "C" {
|
||
|
#[derive(Clone)]
|
||
|
type StreamLanguage;
|
||
|
|
||
|
#[wasm_bindgen(js_namespace = StreamLanguage, js_name = define)]
|
||
|
fn streamlanguage_define(input: StreamLanguage) -> CMExtension;
|
||
|
}
|
||
|
|
||
|
#[function_component(EditorArea)]
|
||
|
fn editor_area(props: &EditorViewProps) -> Html {
|
||
|
let node_ref = use_node_ref();
|
||
|
use_effect_with(node_ref.clone(), |node_ref| match node_ref.get() {
|
||
|
None => {}
|
||
|
Some(node) => {
|
||
|
EditorView::new(EditorViewConfig {
|
||
|
doc: "Hello World".to_owned(),
|
||
|
extensions: vec![
|
||
|
BASIC_SETUP.with(CMExtension::clone),
|
||
|
StreamLanguage::streamlanguage_define(LUA.with(StreamLanguage::clone)),
|
||
|
],
|
||
|
parent: node,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
html! {
|
||
|
<div class="editorarea" ref={node_ref.clone()}>
|
||
|
</div>
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[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! {
|
||
|
<div class="errorbar alert alert-danger alert-dismissible" role="alert">
|
||
|
<span class="errorbar-msg">{props.msg.clone()}</span>
|
||
|
<button class="btn-close" aria-label="Close" onclick={move |_ev| props.dismiss.emit(())}></button>
|
||
|
</div>
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[function_component(CodeEditorView)]
|
||
|
pub fn editor_view(props: &EditorViewProps) -> Html {
|
||
|
let editor_state: UseStateHandle<EditorState> = use_state_eq(fetch_initial_editor_state);
|
||
|
|
||
|
html! {
|
||
|
<>
|
||
|
{match editor_state.error_msg.as_ref() {
|
||
|
None => { html! { <></> }}
|
||
|
Some(msg) => html! {
|
||
|
<ErrorBar msg={msg.clone()}
|
||
|
dismiss={move |()| editor_state.set(EditorState {
|
||
|
error_msg: None,
|
||
|
..(editor_state.deref().clone())
|
||
|
})
|
||
|
}/> }
|
||
|
}}
|
||
|
<SplitPanel direction={PanelDirection::Horizontal}
|
||
|
first={html!{<EditorNav ..props.clone()/>}}
|
||
|
second={html!{<EditorArea ..props.clone()/>}}
|
||
|
/>
|
||
|
</>
|
||
|
}
|
||
|
}
|