package org.openautonomousconnection.luascript.runtime; import javafx.concurrent.Worker; import javafx.scene.web.WebEngine; import org.luaj.vm2.Globals; import org.openautonomousconnection.luascript.fx.FxDomHost; import org.openautonomousconnection.luascript.fx.FxEventHost; import org.openautonomousconnection.luascript.fx.FxUiHost; import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost; import org.openautonomousconnection.luascript.hosts.AudioHost; import org.openautonomousconnection.luascript.hosts.HostServices; import org.openautonomousconnection.luascript.security.LuaExecutionPolicy; import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** * JavaFX WebView integration entry point for LuaScript (no JavaScript). * *
Hard rule: every HTML script tag is treated as Lua.
*/ public final class FxLuaScriptEngine implements AutoCloseable { private final WebEngine engine; private final LuaExecutionPolicy policy; private final AtomicBoolean bootstrapped = new AtomicBoolean(false); private LuaRuntime runtime; /** * Creates an integration engine with default UI execution policy. * * @param engine web engine */ public FxLuaScriptEngine(WebEngine engine) { this(engine, LuaExecutionPolicy.uiDefault()); } /** * Creates an integration engine with a custom execution policy. * * @param engine web engine * @param policy execution policy */ public FxLuaScriptEngine(WebEngine engine, LuaExecutionPolicy policy) { this.engine = Objects.requireNonNull(engine, "engine"); this.policy = Objects.requireNonNull(policy, "policy"); } /** * Installs a load hook that bootstraps Lua when a page finished loading. */ public void install() { engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { if (newState == Worker.State.SUCCEEDED) { bootstrapped.set(false); bootstrap(); } else if (newState == Worker.State.CANCELLED || newState == Worker.State.FAILED) { bootstrapped.set(false); closeRuntimeQuietly(); } }); } /** * Bootstraps Lua for the currently loaded document. */ public void bootstrap() { if (!bootstrapped.compareAndSet(false, true)) return; closeRuntimeQuietly(); // DOM host must exist before event/UI tables, and must ensure stable ids. FxDomHost dom = new FxDomHost(engine); dom.ensureAllElementsHaveId(); // Create per-page globals; harden sandbox in production. Globals globals = LuaGlobalsFactory.create( new LuaGlobalsFactory.Options() .enableDebug(false) .sandbox(true) ); HostServices.StdoutConsole console = new HostServices.StdoutConsole("[lua] "); FxUiHost uiHost = new FxUiHost(engine, dom); FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine); FxEventHost eventHost = new FxEventHost(dom); // TODO: Default implementation or parameter for "audioHost" HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost); LuaRuntime rt = new LuaRuntime(globals, services, policy); eventHost.setRouter(rt.eventRouter()); rt.installStdTables(true); rt.bootstrapFromDom(); this.runtime = rt; } /** * Returns active runtime or null if not bootstrapped. * * @return runtime or null */ public LuaRuntime runtimeOrNull() { return runtime; } @Override public void close() { closeRuntimeQuietly(); } private void closeRuntimeQuietly() { LuaRuntime rt = this.runtime; this.runtime = null; if (rt != null) { try { rt.close(); } catch (Exception ignored) { // Best-effort shutdown. } } } }