deft 0.15.0

Cross platform ui framework
use quick_js::{Callback, Context, ExecutionError, JsValue, ValueError};
use std::cell::RefCell;
use std::future::Future;
use std::panic::RefUnwindSafe;
use std::path::PathBuf;
use tokio::runtime::Builder;
use winit::dpi::{PhysicalSize};
use winit::event::{DeviceEvent, DeviceId, ElementState, WindowEvent};
use winit::window::{WindowId};

use crate::app::App;
use crate::console::Console;
use crate::element::button::Button;
use crate::element::checkbox::Checkbox;
use crate::element::image::Image;
use crate::element::label::Label;
use crate::element::radio::Radio;
use crate::element::richtext::RichText;
use crate::element::scroll::Scroll;
use crate::element::select::Select;
use crate::element::textedit::TextEdit;
use crate::element::textinput::TextInput;
use crate::element::{init_base_components, Element, CSS_MANAGER};
use crate::ext::ext_animation::animation_create;
use crate::ext::ext_app::{AppReopenEvent, JsApp};
#[cfg(fs_enabled)]
use crate::ext::ext_appfs::appfs;
use crate::ext::ext_base64::Base64;
use crate::ext::ext_console::Console as ExtConsole;
use crate::ext::ext_env::env;
#[cfg(fs_enabled)]
use crate::ext::ext_fs::{
    fs_create_dir, fs_create_dir_all, fs_delete_file, fs_exists, fs_read_dir, fs_remove_dir,
    fs_remove_dir_all, fs_rename, fs_stat,
};
use crate::ext::ext_localstorage::localstorage;
use crate::ext::ext_path::path;
use crate::ext::ext_process::process;
use crate::ext::ext_shell::shell;
use crate::ext::ext_timer::{
    timer_clear_interval, timer_clear_timeout, timer_set_interval, timer_set_timeout,
};
#[cfg(feature = "tray")]
use crate::ext::ext_tray::SystemTray;
use crate::ext::ext_window::{handle_window_event, WINDOWS};
use crate::ext::ext_worker::{SharedModuleLoader, Worker, WorkerInitParams};
use crate::js::js_binding::{JsCallError, JsFunc};
use crate::js::js_runtime::{JsContext, PromiseResolver};
use crate::js::ToJsCallResult;
use crate::menu::{Menu, StandardMenuItem};
use crate::mrc::Mrc;
use crate::stylesheet::{stylesheet_add, stylesheet_remove, stylesheet_update};
use crate::typeface::typeface_create;
use crate::window::page::Page;
use crate::window::popup::Popup;
use crate::window::{Window, WindowHandle, WindowType};

thread_local! {
    static JS_ENGINE: RefCell<Option<Mrc<JsEngine>>> = RefCell::new(None);
}

pub struct JsEngine {
    pub js_context: Mrc<JsContext>,
    pub app: App,
}

struct JsFuncCallback {
    js_context: Mrc<JsContext>,
    pub js_func: Box<dyn JsFunc + RefUnwindSafe>,
}

impl Callback<()> for JsFuncCallback {
    fn argument_count(&self) -> usize {
        self.js_func.args_count()
    }

    fn call(&self, args: Vec<JsValue>) -> Result<Result<JsValue, String>, ValueError> {
        let mut js_context = self.js_context.clone();
        match self.js_func.call(&mut js_context, args) {
            Ok(v) => Ok(Ok(v)),
            Err(e) => match e {
                JsCallError::ConversionError(ce) => Err(ce),
                JsCallError::ExecutionError(ee) => Ok(Err(ee.to_string())),
            },
        }
    }
}

impl JsEngine {
    pub fn get() -> Mrc<JsEngine> {
        JS_ENGINE.with(|e| {
            let e = e.borrow();
            let js_engine = e.as_ref().expect("js engine not initialized");
            js_engine.clone()
        })
    }

    pub fn init(app: App) {
        let loader = {
            let mut app = app.app_impl.lock().unwrap();
            SharedModuleLoader::new(app.create_module_loader())
        };
        #[cfg(not(emscripten_platform))]
        let runtime = {
            Builder::new_multi_thread()
                .worker_threads(4)
                .enable_all()
                .build()
                .unwrap()
        };
        #[cfg(emscripten_platform)]
        let runtime = { Builder::new_current_thread().enable_all().build().unwrap() };

        let js_context = Context::builder()
            .console(Console::new())
            .module_loader(loader.clone())
            .build()
            .unwrap();
        let js_context = Mrc::new(JsContext::new(js_context, runtime));

        let engine = Self {
            js_context,
            app: app.clone(),
        };

        init_base_components();
        engine.add_global_functions(Popup::create_js_apis());

        engine.add_global_functions(Menu::create_js_apis());
        engine.add_global_functions(StandardMenuItem::create_js_apis());

        engine.add_global_functions(Page::create_js_apis());
        engine.add_global_functions(ExtConsole::create_js_apis());
        engine.add_global_functions(Element::create_js_apis());
        engine.add_global_functions(Scroll::create_js_apis());
        engine.add_global_functions(Button::create_js_apis());
        engine.add_global_functions(Checkbox::create_js_apis());
        engine.add_global_functions(Radio::create_js_apis());
        engine.add_global_functions(TextInput::create_js_apis());
        engine.add_global_functions(TextEdit::create_js_apis());
        engine.add_global_functions(RichText::create_js_apis());
        engine.add_global_functions(Label::create_js_apis());
        engine.add_global_functions(Image::create_js_apis());
        engine.add_global_functions(Select::create_js_apis());
        #[cfg(feature = "sqlite")]
        engine.add_global_functions(crate::ext::ext_sqlite::SqliteConn::create_js_apis());
        #[cfg(feature = "tray")]
        {
            engine.add_global_functions(SystemTray::create_js_apis());
        }
        engine.add_global_functions(process::create_js_apis());
        #[cfg(feature = "dialog")]
        engine.add_global_functions(crate::ext::ext_dialog::dialog::create_js_apis());
        engine.add_global_functions(Base64::create_js_apis());
        engine.add_global_functions(shell::create_js_apis());
        #[cfg(feature = "audio")]
        engine.add_global_functions(crate::ext::ext_audio::Audio::create_js_apis());
        engine.add_global_functions(path::create_js_apis());
        engine.add_global_functions(env::create_js_apis());
        #[cfg(feature = "http")]
        engine.add_global_functions(crate::ext::ext_http::http::create_js_apis());
        #[cfg(fs_enabled)]
        engine.add_global_functions(appfs::create_js_apis());
        engine.add_global_functions(localstorage::create_js_apis());
        // websocket
        #[cfg(feature = "websocket")]
        engine.add_global_functions(crate::ext::ext_websocket::WsConnection::create_js_apis());
        #[cfg(feature = "http")]
        engine.add_global_functions(crate::ext::ext_fetch::fetch::create_js_apis());

        engine.add_global_functions(Window::create_js_apis());
        engine.add_global_func(timer_set_timeout::new());
        engine.add_global_func(timer_clear_timeout::new());
        engine.add_global_func(timer_set_interval::new());
        engine.add_global_func(timer_clear_interval::new());

        #[cfg(fs_enabled)]
        {
            engine.add_global_func(fs_read_dir::new());
            engine.add_global_func(fs_stat::new());
            engine.add_global_func(fs_exists::new());
            engine.add_global_func(fs_rename::new());
            engine.add_global_func(fs_delete_file::new());
            engine.add_global_func(fs_create_dir::new());
            engine.add_global_func(fs_create_dir_all::new());
            engine.add_global_func(fs_remove_dir::new());
            engine.add_global_func(fs_remove_dir_all::new());
        }

        engine.add_global_func(animation_create::new());
        engine.add_global_func(typeface_create::new());

        #[cfg(feature = "clipboard")]
        engine.add_global_functions(crate::ext::ext_clipboard::Clipboard::create_js_apis());
        engine.add_global_func(stylesheet_add::new());
        engine.add_global_func(stylesheet_remove::new());
        engine.add_global_func(stylesheet_update::new());

        Worker::init_js_api(WorkerInitParams { app });
        engine.add_global_functions(Worker::create_js_apis());


        engine.add_global_functions(JsApp::create_js_apis());

        JS_ENGINE.with(|e| *e.borrow_mut() = Some(Mrc::new(engine)));
    }

    pub fn enable_localstorage(&mut self, p: PathBuf) {
        localstorage::init(p);
    }

    pub fn create_async_task<F, O>(&mut self, future: F) -> JsValue
    where
        F: Future<Output = O> + Send + 'static,
        O: ToJsCallResult,
    {
        self.js_context.create_async_task2(future)
    }

    pub fn create_promise(&mut self) -> (JsValue, PromiseResolver) {
        self.js_context.create_promise()
    }

    pub fn init_api(&self) {
        let default_css = include_str!("../../deft.css");
        CSS_MANAGER.with_borrow_mut(|manager| {
            if let Err(e) = manager.add(default_css) {
                println!("Error adding CSS: {:?}", e);
            }
        });
        let libjs = String::from_utf8_lossy(include_bytes!("../../lib.js"));
        self.js_context.eval_module(&libjs, "lib.js").unwrap();
    }

    pub fn add_global_functions(&self, functions: Vec<Box<dyn JsFunc + RefUnwindSafe + 'static>>) {
        for func in functions {
            let name = func.name().to_string();
            let js_context = self.js_context.clone();
            self.js_context
                .add_callback(
                    name.as_str(),
                    JsFuncCallback {
                        js_func: func,
                        js_context,
                    },
                )
                .unwrap();
        }
    }

    pub fn add_global_func(&self, func: impl JsFunc + RefUnwindSafe + 'static) {
        let name = func.name().to_string();
        let js_context = self.js_context.clone();
        self.js_context
            .add_callback(
                name.as_str(),
                JsFuncCallback {
                    js_func: Box::new(func),
                    js_context,
                },
            )
            .unwrap();
    }

    pub fn execute_main(&mut self) {
        self.js_context.execute_main();
    }

    pub fn execute_module(&mut self, module_name: &str) -> Result<(), ExecutionError> {
        self.js_context.execute_module(module_name)
    }

    pub fn eval_module(&mut self, code: &str, filename: &str) -> Result<JsValue, ExecutionError> {
        self.js_context.eval_module(code, filename)
    }

    pub fn handle_window_event(&mut self, window_id: WindowId, event: WindowEvent) {
        handle_window_event(window_id, event);
    }

    pub fn handle_device_event(&mut self, _device_id: DeviceId, event: DeviceEvent) {
        if let DeviceEvent::Button { state, .. } = event {
            if state == ElementState::Pressed {
                let close_windows: Vec<WindowHandle> = WINDOWS.with_borrow(|windows| {
                    windows
                        .iter()
                        .filter(|(_, f)| {
                            f.upgrade_mut()
                                .ok()
                                .map(|f| f.window_type == WindowType::Menu)
                                .unwrap_or(false)
                        })
                        .map(|(_, f)| f.clone())
                        .filter(|w| {
                            if let Ok(window) = w.upgrade_mut() {
                                let w_size: PhysicalSize<i32> = window.window.outer_size().cast();
                                if let Some(ptr_pos) = window.window.pointer_position() {
                                    let is_in_window = ptr_pos.x >= 0
                                        && ptr_pos.x <= w_size.width
                                        && ptr_pos.y >= 0
                                        && ptr_pos.y <= w_size.height;
                                    if !is_in_window {
                                        return true;
                                    }
                                }
                            }
                            false
                        })
                        .collect()
                });
                for f in close_windows {
                    if let Ok(mut f) = f.upgrade_mut() {
                        let _ = f.close();
                    }
                }
            }
        }
    }

    pub fn handle_reopen(&mut self, has_visible: bool) {
        JsApp::emit(AppReopenEvent { has_visible });
    }

    pub fn execute_pending_jobs(&self) {
        let jc = self.js_context.clone();
        loop {
            let job_res = jc.execute_pending_job();
            match job_res {
                Ok(res) => {
                    if !res {
                        break;
                    }
                }
                Err(e) => {
                    eprint!("job error:{:?}", e);
                    break;
                }
            }
        }
    }
}