add native window/webview builtins to el runtime

Adds five new builtins backed by wry 0.47 + tao 0.30:
- window_open(title, width, height) — configure window
- webview_load_url(url) — set URL to navigate to
- webview_load_html(html) — set HTML to display
- webview_eval(js) — queue JS to eval after load
- window_run() — open native OS window and run event loop (blocking, exits on close)

WebViewState is stored in a thread_local. window_run() calls
tao's event_loop.run() which never returns; process exits when
the window is closed via std::process::exit(0).
This commit is contained in:
Will Anderson
2026-04-28 14:14:34 -05:00
parent 07cfde2402
commit b9d096ef5b
4 changed files with 2189 additions and 61 deletions
Generated
+2081 -61
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -34,3 +34,5 @@ hmac = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
wry = "0.47"
tao = "0.30"
+99
View File
@@ -56,6 +56,29 @@ thread_local! {
std::cell::RefCell::new(None);
}
// ── Native window / WebView state ────────────────────────────────────────────
#[derive(Default)]
struct WebViewState {
title: String,
width: u32,
height: u32,
url: Option<String>,
html: Option<String>,
pending_js: Vec<String>,
}
thread_local! {
static WV_STATE: std::cell::RefCell<WebViewState> = std::cell::RefCell::new(WebViewState {
title: "Neuron".to_string(),
width: 1200,
height: 800,
url: None,
html: None,
pending_js: Vec::new(),
});
}
// ── CLI definition ────────────────────────────────────────────────────────────
#[derive(Parser, Debug)]
@@ -4163,6 +4186,82 @@ fn dispatch_builtin(
BuiltinResult::Handled
}
// ── Native window / WebView builtins ──────────────────────────────────
"window_open" => {
// window_open(title: String, width: Int, height: Int) -> Void
let height = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 800 };
let width = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 1200 };
let title = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "Neuron".to_string() };
WV_STATE.with(|st| {
let mut s = st.borrow_mut();
s.title = title;
s.width = width;
s.height = height;
});
BuiltinResult::Handled
}
"webview_load_url" => {
// webview_load_url(url: String) -> Void
let url = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { return BuiltinResult::Handled; } };
WV_STATE.with(|st| st.borrow_mut().url = Some(url));
BuiltinResult::Handled
}
"webview_load_html" => {
// webview_load_html(html: String) -> Void
let html = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { return BuiltinResult::Handled; } };
WV_STATE.with(|st| st.borrow_mut().html = Some(html));
BuiltinResult::Handled
}
"webview_eval" => {
// webview_eval(js: String) -> Void
let js = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { return BuiltinResult::Handled; } };
WV_STATE.with(|st| st.borrow_mut().pending_js.push(js));
BuiltinResult::Handled
}
"window_run" => {
// window_run() -> Void (never returns; exits process on window close)
use tao::event::{Event, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoop};
use tao::window::WindowBuilder;
use wry::WebViewBuilder;
let (title, width, height, url, html) = WV_STATE.with(|st| {
let s = st.borrow();
(s.title.clone(), s.width, s.height, s.url.clone(), s.html.clone())
});
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title(title)
.with_inner_size(tao::dpi::LogicalSize::new(width, height))
.build(&event_loop)
.expect("failed to create window");
let mut builder = WebViewBuilder::new();
if let Some(u) = url {
builder = builder.with_url(u);
} else if let Some(h) = html {
builder = builder.with_html(h);
} else {
builder = builder.with_html("<h1>Neuron</h1>");
}
let _webview = builder.build(&window).expect("failed to create webview");
// event_loop.run() never returns (diverging fn `-> !`)
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
if let Event::WindowEvent { event: WindowEvent::CloseRequested, .. } = event {
std::process::exit(0);
}
});
}
_ => BuiltinResult::NotBuiltin,
}
}
+7
View File
@@ -262,6 +262,13 @@ impl TypeEnv {
env.functions.insert("cursor_col".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("http_sse_post".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
// Native window / WebView builtins
env.functions.insert("window_open".into(), str_fn(vec![s.clone(), i.clone(), i.clone()], Type::Void));
env.functions.insert("webview_load_url".into(), str_fn(vec![s.clone()], Type::Void));
env.functions.insert("webview_load_html".into(), str_fn(vec![s.clone()], Type::Void));
env.functions.insert("webview_eval".into(), str_fn(vec![s.clone()], Type::Void));
env.functions.insert("window_run".into(), str_fn(vec![], Type::Void));
// Math
for name in &["math_abs","math_floor","math_ceil","math_round","math_sqrt"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));