replace webview with 2D canvas builtins (winit + tiny-skia + fontdue)
Removes wry/tao WebView layer. Adds a pixel-level canvas API so Engram programs can drive the window directly — fill rects, draw text, handle mouse/keyboard input — enabling the UI framework to be written in Engram. Dependencies: winit 0.29 (rwh_05), softbuffer 0.3, tiny-skia 0.11, fontdue 0.8.
This commit is contained in:
Generated
+733
-1564
File diff suppressed because it is too large
Load Diff
@@ -58,3 +58,7 @@ hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
winit = { version = "0.29", default-features = false, features = ["rwh_05"] }
|
||||
softbuffer = "0.3"
|
||||
tiny-skia = "0.11"
|
||||
fontdue = "0.8"
|
||||
|
||||
+4
-2
@@ -34,5 +34,7 @@ hmac = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
wry = "0.47"
|
||||
tao = "0.30"
|
||||
winit = { version = "0.29", default-features = false, features = ["rwh_05"] }
|
||||
softbuffer = "0.3"
|
||||
tiny-skia = "0.11"
|
||||
fontdue = "0.8"
|
||||
|
||||
+499
-62
@@ -56,26 +56,40 @@ thread_local! {
|
||||
std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
// ── Native window / WebView state ────────────────────────────────────────────
|
||||
// ── Canvas / native window state ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Default)]
|
||||
struct WebViewState {
|
||||
title: String,
|
||||
width: u32,
|
||||
height: u32,
|
||||
url: Option<String>,
|
||||
html: Option<String>,
|
||||
pending_js: Vec<String>,
|
||||
struct CanvasState {
|
||||
// Window configuration
|
||||
title: String,
|
||||
width: u32,
|
||||
height: u32,
|
||||
// Drawing surface (tiny-skia pixmap)
|
||||
pixmap: Option<tiny_skia::Pixmap>,
|
||||
// Font for text rendering (fontdue)
|
||||
font: Option<fontdue::Font>,
|
||||
// Input state
|
||||
mouse_x: i32,
|
||||
mouse_y: i32,
|
||||
mouse_buttons: u8, // bit 0 = left, bit 1 = right
|
||||
// Events accumulated since last frame (JSON strings)
|
||||
events: Vec<String>,
|
||||
// Clip rectangle stack: (x, y, w, h)
|
||||
clips: Vec<(i32, i32, u32, u32)>,
|
||||
}
|
||||
|
||||
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(),
|
||||
static CANVAS: std::cell::RefCell<CanvasState> = std::cell::RefCell::new(CanvasState {
|
||||
title: String::new(),
|
||||
width: 1200,
|
||||
height: 800,
|
||||
pixmap: None,
|
||||
font: None,
|
||||
mouse_x: 0,
|
||||
mouse_y: 0,
|
||||
mouse_buttons: 0,
|
||||
events: Vec::new(),
|
||||
clips: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1721,6 +1735,37 @@ fn soma_reason(query: &str) -> String {
|
||||
.unwrap_or_else(|| "[soma unavailable]".into())
|
||||
}
|
||||
|
||||
// ── Canvas helper functions ───────────────────────────────────────────────────
|
||||
|
||||
/// Parse "#rrggbb" or "#rrggbbaa" into tiny-skia Color.
|
||||
fn parse_color(s: &str) -> tiny_skia::Color {
|
||||
let s = s.trim_start_matches('#');
|
||||
let r = u8::from_str_radix(s.get(0..2).unwrap_or("ff"), 16).unwrap_or(255);
|
||||
let g = u8::from_str_radix(s.get(2..4).unwrap_or("ff"), 16).unwrap_or(255);
|
||||
let b = u8::from_str_radix(s.get(4..6).unwrap_or("ff"), 16).unwrap_or(255);
|
||||
let a = u8::from_str_radix(s.get(6..8).unwrap_or("ff"), 16).unwrap_or(255);
|
||||
tiny_skia::Color::from_rgba8(r, g, b, a)
|
||||
}
|
||||
|
||||
/// Rasterize `text` at `size` pixels using the stored fontdue font.
|
||||
/// Returns (glyphs: Vec<(x_offset, metrics, bitmap)>, total_width).
|
||||
fn rasterize_text(text: &str, size: f32) -> (Vec<(i32, fontdue::Metrics, Vec<u8>)>, i32) {
|
||||
CANVAS.with(|cv| {
|
||||
let cv = cv.borrow();
|
||||
let font = match &cv.font { Some(f) => f, None => return (vec![], 0) };
|
||||
let mut glyphs = Vec::new();
|
||||
let mut cursor_x = 0i32;
|
||||
for ch in text.chars() {
|
||||
let (metrics, bitmap) = font.rasterize(ch, size);
|
||||
glyphs.push((cursor_x, metrics, bitmap));
|
||||
cursor_x += metrics.advance_width as i32;
|
||||
}
|
||||
(glyphs, cursor_x)
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
enum BuiltinResult {
|
||||
Handled,
|
||||
Exit(i32),
|
||||
@@ -2261,6 +2306,38 @@ fn dispatch_builtin(
|
||||
continue;
|
||||
}
|
||||
|
||||
// GET /assets/* — serve static files adjacent to the HTML file
|
||||
if method == "GET" && path.starts_with("/assets/") {
|
||||
let file_name = &path["/assets/".len()..];
|
||||
let html_dir = GLOBAL_STATE.with(|gs| {
|
||||
gs.borrow().get("__html_file__").and_then(|p| {
|
||||
std::path::Path::new(p).parent().map(|d| d.to_path_buf())
|
||||
})
|
||||
});
|
||||
let content_type = if file_name.ends_with(".png") { "image/png" }
|
||||
else if file_name.ends_with(".svg") { "image/svg+xml" }
|
||||
else if file_name.ends_with(".jpg") || file_name.ends_with(".jpeg") { "image/jpeg" }
|
||||
else { "application/octet-stream" };
|
||||
let ct_header = tiny_http::Header::from_bytes(
|
||||
"Content-Type", content_type
|
||||
).unwrap();
|
||||
if let Some(dir) = html_dir {
|
||||
let asset_path = dir.join(file_name);
|
||||
if let Ok(bytes) = std::fs::read(&asset_path) {
|
||||
let _ = request.respond(
|
||||
tiny_http::Response::from_data(bytes)
|
||||
.with_header(ct_header)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let _ = request.respond(
|
||||
tiny_http::Response::from_string("not found")
|
||||
.with_status_code(404u16)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// POST /api/chat — proxy to Neuron runtime (SSE → collect → JSON)
|
||||
// Transforms landing page format { message, history, conv_id }
|
||||
// into runtime format { messages: [{role,content}], conv_id }
|
||||
@@ -4186,80 +4263,440 @@ fn dispatch_builtin(
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
// ── Native window / WebView builtins ──────────────────────────────────
|
||||
// ── Canvas builtins ───────────────────────────────────────────────────
|
||||
|
||||
"window_open" => {
|
||||
// window_open(title: String, width: Int, height: Int) -> Void
|
||||
"canvas_open" => {
|
||||
// canvas_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;
|
||||
let title = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "Neuron".into() };
|
||||
|
||||
// Load font (try SF NS, Arial, DejaVu in order)
|
||||
let font_data =
|
||||
std::fs::read("/System/Library/Fonts/SFNS.ttf")
|
||||
.or_else(|_| std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf"))
|
||||
.or_else(|_| std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"))
|
||||
.unwrap_or_default();
|
||||
let font = if font_data.is_empty() { None } else {
|
||||
fontdue::Font::from_bytes(font_data.as_slice(), fontdue::FontSettings::default()).ok()
|
||||
};
|
||||
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
cv.title = title;
|
||||
cv.width = width;
|
||||
cv.height = height;
|
||||
cv.font = font;
|
||||
cv.pixmap = tiny_skia::Pixmap::new(width, height);
|
||||
});
|
||||
stack.push(Value::Nil);
|
||||
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));
|
||||
"canvas_clear" => {
|
||||
// canvas_clear(color: String) -> Void
|
||||
let color_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#000000".into() };
|
||||
let color = parse_color(&color_str);
|
||||
CANVAS.with(|cv| {
|
||||
if let Some(px) = cv.borrow_mut().pixmap.as_mut() {
|
||||
px.fill(color);
|
||||
}
|
||||
});
|
||||
stack.push(Value::Nil);
|
||||
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));
|
||||
"canvas_fill_rect" => {
|
||||
// canvas_fill_rect(x, y, w, h: Int, color: String, radius: Int) -> Void
|
||||
let radius = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, Value::Float(f) => f as f32, _ => 0.0 };
|
||||
let color_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
|
||||
let rh = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let rw = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let ry = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let rx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let color = parse_color(&color_str);
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
// Collect clip info before mutably borrowing pixmap
|
||||
let clip_rect = cv.clips.last().copied();
|
||||
if let Some(px) = cv.pixmap.as_mut() {
|
||||
let mut paint = tiny_skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
paint.anti_alias = true;
|
||||
let path = {
|
||||
let mut pb = tiny_skia::PathBuilder::new();
|
||||
if radius > 0.0 {
|
||||
let r = radius.min(rw / 2.0).min(rh / 2.0);
|
||||
pb.move_to(rx + r, ry);
|
||||
pb.line_to(rx + rw - r, ry);
|
||||
pb.quad_to(rx + rw, ry, rx + rw, ry + r);
|
||||
pb.line_to(rx + rw, ry + rh - r);
|
||||
pb.quad_to(rx + rw, ry + rh, rx + rw - r, ry + rh);
|
||||
pb.line_to(rx + r, ry + rh);
|
||||
pb.quad_to(rx, ry + rh, rx, ry + rh - r);
|
||||
pb.line_to(rx, ry + r);
|
||||
pb.quad_to(rx, ry, rx + r, ry);
|
||||
pb.close();
|
||||
} else if let Some(rect) = tiny_skia::Rect::from_xywh(rx, ry, rw.max(0.1), rh.max(0.1)) {
|
||||
pb.push_rect(rect);
|
||||
}
|
||||
pb.finish()
|
||||
};
|
||||
if let Some(path) = path {
|
||||
// Apply clip if any
|
||||
let clip_mask = clip_rect.and_then(|(cx, cy, cw, ch)| {
|
||||
tiny_skia::Rect::from_xywh(cx as f32, cy as f32, cw as f32, ch as f32).and_then(|rect| {
|
||||
let clip_path = tiny_skia::PathBuilder::from_rect(rect);
|
||||
let mut mask = tiny_skia::Mask::new(px.width(), px.height())?;
|
||||
mask.fill_path(&clip_path, tiny_skia::FillRule::Winding, false, tiny_skia::Transform::identity());
|
||||
Some(mask)
|
||||
})
|
||||
});
|
||||
px.fill_path(&path, &paint, tiny_skia::FillRule::Winding, tiny_skia::Transform::identity(), clip_mask.as_ref());
|
||||
}
|
||||
}
|
||||
});
|
||||
stack.push(Value::Nil);
|
||||
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));
|
||||
"canvas_stroke_rect" => {
|
||||
// canvas_stroke_rect(x, y, w, h: Int, color: String, stroke_w: Int, radius: Int) -> Void
|
||||
let radius = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, Value::Float(f) => f as f32, _ => 0.0 };
|
||||
let stroke_w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 1.0 };
|
||||
let color_str = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
|
||||
let rh = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let rw = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let ry = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let rx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let color = parse_color(&color_str);
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
if let Some(px) = cv.pixmap.as_mut() {
|
||||
let mut paint = tiny_skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
paint.anti_alias = true;
|
||||
let mut stroke = tiny_skia::Stroke::default();
|
||||
stroke.width = stroke_w;
|
||||
let path = {
|
||||
let mut pb = tiny_skia::PathBuilder::new();
|
||||
if radius > 0.0 {
|
||||
let r = radius.min(rw / 2.0).min(rh / 2.0);
|
||||
pb.move_to(rx + r, ry);
|
||||
pb.line_to(rx + rw - r, ry);
|
||||
pb.quad_to(rx + rw, ry, rx + rw, ry + r);
|
||||
pb.line_to(rx + rw, ry + rh - r);
|
||||
pb.quad_to(rx + rw, ry + rh, rx + rw - r, ry + rh);
|
||||
pb.line_to(rx + r, ry + rh);
|
||||
pb.quad_to(rx, ry + rh, rx, ry + rh - r);
|
||||
pb.line_to(rx, ry + r);
|
||||
pb.quad_to(rx, ry, rx + r, ry);
|
||||
pb.close();
|
||||
} else if let Some(rect) = tiny_skia::Rect::from_xywh(rx, ry, rw.max(0.1), rh.max(0.1)) {
|
||||
pb.push_rect(rect);
|
||||
}
|
||||
pb.finish()
|
||||
};
|
||||
if let Some(path) = path {
|
||||
px.stroke_path(&path, &paint, &stroke, tiny_skia::Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
});
|
||||
stack.push(Value::Nil);
|
||||
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;
|
||||
"canvas_line" => {
|
||||
// canvas_line(x1, y1, x2, y2: Int, color: String, line_w: Int) -> Void
|
||||
let line_w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 1.0 };
|
||||
let color_s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
|
||||
let y2 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let x2 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let y1 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let x1 = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 0.0 };
|
||||
let color = parse_color(&color_s);
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
if let Some(px) = cv.pixmap.as_mut() {
|
||||
let mut paint = tiny_skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
paint.anti_alias = true;
|
||||
let mut stroke = tiny_skia::Stroke::default();
|
||||
stroke.width = line_w;
|
||||
let mut pb = tiny_skia::PathBuilder::new();
|
||||
pb.move_to(x1, y1);
|
||||
pb.line_to(x2, y2);
|
||||
if let Some(path) = pb.finish() {
|
||||
px.stroke_path(&path, &paint, &stroke, tiny_skia::Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
});
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
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())
|
||||
"canvas_text" => {
|
||||
// canvas_text(x, y: Int, text: String, size: Int, color: String) -> Void
|
||||
let color_s = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "#ffffff".into() };
|
||||
let size = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 14.0 };
|
||||
let text = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
|
||||
let y = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
|
||||
let x = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
|
||||
let color = parse_color(&color_s);
|
||||
|
||||
let (glyphs, _) = rasterize_text(&text, size);
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
if let Some(px) = cv.pixmap.as_mut() {
|
||||
let pw = px.width() as i32;
|
||||
let ph = px.height() as i32;
|
||||
let cr = (color.red() * 255.0) as u32;
|
||||
let cg = (color.green() * 255.0) as u32;
|
||||
let cb = (color.blue() * 255.0) as u32;
|
||||
for (gx_off, metrics, bitmap) in &glyphs {
|
||||
let gx = x + gx_off + metrics.xmin;
|
||||
let gy = y - metrics.height as i32 - metrics.ymin;
|
||||
for row in 0..metrics.height {
|
||||
for col in 0..metrics.width {
|
||||
let alpha = bitmap[row * metrics.width + col];
|
||||
if alpha == 0 { continue; }
|
||||
let px_x = gx + col as i32;
|
||||
let px_y = gy + row as i32;
|
||||
if px_x < 0 || px_y < 0 || px_x >= pw || px_y >= ph { continue; }
|
||||
let idx = (px_y as usize * pw as usize + px_x as usize) * 4;
|
||||
let data = px.data_mut();
|
||||
let a = alpha as u32;
|
||||
let ia = 255 - a;
|
||||
data[idx] = ((ia * data[idx] as u32 + a * cr) / 255) as u8;
|
||||
data[idx + 1] = ((ia * data[idx+1] as u32 + a * cg) / 255) as u8;
|
||||
data[idx + 2] = ((ia * data[idx+2] as u32 + a * cb) / 255) as u8;
|
||||
data[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_text_width" => {
|
||||
// canvas_text_width(text: String, size: Int) -> Int
|
||||
let size = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 14.0 };
|
||||
let text = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => String::new() };
|
||||
let (_, w) = rasterize_text(&text, size);
|
||||
stack.push(Value::Int(w as i64));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_text_height" => {
|
||||
// canvas_text_height(size: Int) -> Int
|
||||
let size = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as f32, _ => 14.0 };
|
||||
stack.push(Value::Int((size * 1.2) as i64));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_clip" => {
|
||||
// canvas_clip(x, y, w, h: Int) -> Void
|
||||
let h = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 0 };
|
||||
let w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as u32, _ => 0 };
|
||||
let y = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
|
||||
let x = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
|
||||
CANVAS.with(|cv| cv.borrow_mut().clips.push((x, y, w, h)));
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_unclip" => {
|
||||
CANVAS.with(|cv| { cv.borrow_mut().clips.pop(); });
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_size" => {
|
||||
// canvas_size() -> [Int] — [width, height]
|
||||
let (w, h) = CANVAS.with(|cv| {
|
||||
let cv = cv.borrow();
|
||||
(cv.width as i64, cv.height as i64)
|
||||
});
|
||||
stack.push(Value::List(vec![Value::Int(w), Value::Int(h)]));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_mouse_pos" => {
|
||||
// canvas_mouse_pos() -> [Int] — [x, y]
|
||||
let (x, y) = CANVAS.with(|cv| {
|
||||
let cv = cv.borrow();
|
||||
(cv.mouse_x as i64, cv.mouse_y as i64)
|
||||
});
|
||||
stack.push(Value::List(vec![Value::Int(x), Value::Int(y)]));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_events" => {
|
||||
// canvas_events() -> String — JSON array of event objects
|
||||
let evts = CANVAS.with(|cv| cv.borrow().events.clone());
|
||||
let result = if evts.is_empty() {
|
||||
"[]".to_string()
|
||||
} else {
|
||||
format!("[{}]", evts.join(","))
|
||||
};
|
||||
stack.push(Value::Str(result));
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_swap" => {
|
||||
// canvas_swap() -> Void — no-op; canvas_run_loop handles presentation
|
||||
stack.push(Value::Nil);
|
||||
BuiltinResult::Handled
|
||||
}
|
||||
|
||||
"canvas_run_loop" => {
|
||||
// canvas_run_loop(draw_fn: String) -> Void (never returns)
|
||||
let draw_fn = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => "draw".into() };
|
||||
|
||||
let arc_instructions = SERVE_INSTRUCTIONS.with(|si| si.borrow().clone())
|
||||
.expect("canvas_run_loop: no instructions stored (call canvas_open first and ensure interpreter stored them)");
|
||||
let arc_fn_table = SERVE_FN_TABLE.with(|sf| sf.borrow().clone())
|
||||
.expect("canvas_run_loop: no fn_table stored");
|
||||
|
||||
let (title, width, height) = CANVAS.with(|cv| {
|
||||
let cv = cv.borrow();
|
||||
(cv.title.clone(), cv.width, cv.height)
|
||||
});
|
||||
|
||||
let event_loop = EventLoop::new();
|
||||
use winit::event::{Event, WindowEvent, MouseButton, ElementState};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit::keyboard::{Key, NamedKey};
|
||||
use winit::window::WindowBuilder;
|
||||
|
||||
let event_loop = EventLoop::new().expect("failed to create event loop");
|
||||
let window = WindowBuilder::new()
|
||||
.with_title(title)
|
||||
.with_inner_size(tao::dpi::LogicalSize::new(width, height))
|
||||
.with_title(&title)
|
||||
.with_inner_size(winit::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 context = unsafe { softbuffer::Context::new(&window) }.expect("softbuffer context");
|
||||
let mut surface = unsafe { softbuffer::Surface::new(&context, &window) }.expect("softbuffer surface");
|
||||
|
||||
let _webview = builder.build(&window).expect("failed to create webview");
|
||||
let mut frame_events: Vec<String> = Vec::new();
|
||||
|
||||
// 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);
|
||||
let _ = event_loop.run(move |event, elwt| {
|
||||
elwt.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
match event {
|
||||
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::Resized(physical), .. } => {
|
||||
let nw = physical.width.max(1);
|
||||
let nh = physical.height.max(1);
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
cv.width = nw;
|
||||
cv.height = nh;
|
||||
cv.pixmap = tiny_skia::Pixmap::new(nw, nh);
|
||||
});
|
||||
frame_events.push(format!(r#"{{"type":"resize","w":{},"h":{}}}"#, nw, nh));
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::CursorMoved { position, .. }, .. } => {
|
||||
let mx = position.x as i32;
|
||||
let my = position.y as i32;
|
||||
CANVAS.with(|cv| {
|
||||
let mut cv = cv.borrow_mut();
|
||||
cv.mouse_x = mx;
|
||||
cv.mouse_y = my;
|
||||
});
|
||||
frame_events.push(format!(r#"{{"type":"mouse_move","x":{},"y":{}}}"#, mx, my));
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::MouseInput { state, button, .. }, .. } => {
|
||||
let btn_str = match button { MouseButton::Left => "left", MouseButton::Right => "right", _ => "other" };
|
||||
let ev_type = match state { ElementState::Pressed => "mouse_down", ElementState::Released => "mouse_up" };
|
||||
let (mx, my) = CANVAS.with(|cv| { let cv = cv.borrow(); (cv.mouse_x, cv.mouse_y) });
|
||||
frame_events.push(format!(r#"{{"type":"{}","x":{},"y":{},"button":"{}"}}"#, ev_type, mx, my, btn_str));
|
||||
if state == ElementState::Released && button == MouseButton::Left {
|
||||
frame_events.push(format!(r#"{{"type":"mouse_click","x":{},"y":{},"button":"left"}}"#, mx, my));
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::MouseWheel { delta, .. }, .. } => {
|
||||
let (dx, dy) = match delta {
|
||||
winit::event::MouseScrollDelta::LineDelta(x, y) => (x as i32, y as i32),
|
||||
winit::event::MouseScrollDelta::PixelDelta(p) => (p.x as i32, p.y as i32),
|
||||
};
|
||||
frame_events.push(format!(r#"{{"type":"scroll","dx":{},"dy":{}}}"#, dx, dy));
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::KeyboardInput { event: ref kev, .. }, .. } => {
|
||||
// Emit char events for printable keys
|
||||
if kev.state == ElementState::Pressed {
|
||||
if let Key::Character(ref s) = kev.logical_key {
|
||||
let ch_str = s.as_str();
|
||||
if !ch_str.chars().all(|c| c.is_control()) {
|
||||
let escaped = ch_str.replace('"', "\\\"");
|
||||
frame_events.push(format!(r#"{{"type":"char","char":"{}"}}"#, escaped));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Emit key_down events for named keys
|
||||
if kev.state == ElementState::Pressed {
|
||||
let key_str = match &kev.logical_key {
|
||||
Key::Named(NamedKey::Enter) => "Return",
|
||||
Key::Named(NamedKey::Escape) => "Escape",
|
||||
Key::Named(NamedKey::Backspace) => "Backspace",
|
||||
Key::Named(NamedKey::Delete) => "Delete",
|
||||
Key::Named(NamedKey::ArrowLeft) => "ArrowLeft",
|
||||
Key::Named(NamedKey::ArrowRight) => "ArrowRight",
|
||||
Key::Named(NamedKey::ArrowUp) => "ArrowUp",
|
||||
Key::Named(NamedKey::ArrowDown) => "ArrowDown",
|
||||
Key::Named(NamedKey::Tab) => "Tab",
|
||||
Key::Named(NamedKey::Home) => "Home",
|
||||
Key::Named(NamedKey::End) => "End",
|
||||
_ => "",
|
||||
};
|
||||
if !key_str.is_empty() {
|
||||
frame_events.push(format!(r#"{{"type":"key_down","key":"{}"}}"#, key_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::AboutToWait => {
|
||||
// Set this frame's events, then call the Engram draw function
|
||||
CANVAS.with(|cv| {
|
||||
cv.borrow_mut().events = std::mem::take(&mut frame_events);
|
||||
});
|
||||
|
||||
if let Some(&entry) = arc_fn_table.get(draw_fn.as_str()) {
|
||||
run_sub_interpreter(&arc_instructions, &arc_fn_table, entry);
|
||||
}
|
||||
|
||||
// Present the pixmap to the screen
|
||||
let (w, h) = CANVAS.with(|cv| { let cv = cv.borrow(); (cv.width, cv.height) });
|
||||
let nz_w = std::num::NonZeroU32::new(w.max(1)).unwrap();
|
||||
let nz_h = std::num::NonZeroU32::new(h.max(1)).unwrap();
|
||||
if surface.resize(nz_w, nz_h).is_ok() {
|
||||
if let Ok(mut buf) = surface.buffer_mut() {
|
||||
CANVAS.with(|cv| {
|
||||
let cv = cv.borrow();
|
||||
if let Some(px) = &cv.pixmap {
|
||||
let pixels = px.data(); // RGBA bytes
|
||||
for (i, chunk) in pixels.chunks_exact(4).enumerate() {
|
||||
// softbuffer expects 0x00RRGGBB in native u32
|
||||
buf[i] = ((chunk[0] as u32) << 16)
|
||||
| ((chunk[1] as u32) << 8)
|
||||
| (chunk[2] as u32);
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = buf.present();
|
||||
}
|
||||
}
|
||||
|
||||
window.request_redraw();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
// event_loop.run never returns normally (exits via std::process::exit)
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
_ => BuiltinResult::NotBuiltin,
|
||||
|
||||
@@ -262,12 +262,22 @@ 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));
|
||||
// Canvas / native window builtins
|
||||
env.functions.insert("canvas_open".into(), str_fn(vec![s.clone(), i.clone(), i.clone()], Type::Void));
|
||||
env.functions.insert("canvas_clear".into(), str_fn(vec![s.clone()], Type::Void));
|
||||
env.functions.insert("canvas_fill_rect".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone(), s.clone(), i.clone()], Type::Void));
|
||||
env.functions.insert("canvas_stroke_rect".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone(), s.clone(), i.clone(), i.clone()], Type::Void));
|
||||
env.functions.insert("canvas_line".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone(), s.clone(), i.clone()], Type::Void));
|
||||
env.functions.insert("canvas_text".into(), str_fn(vec![i.clone(), i.clone(), s.clone(), i.clone(), s.clone()], Type::Void));
|
||||
env.functions.insert("canvas_text_width".into(), str_fn(vec![s.clone(), i.clone()], i.clone()));
|
||||
env.functions.insert("canvas_text_height".into(), str_fn(vec![i.clone()], i.clone()));
|
||||
env.functions.insert("canvas_clip".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone()], Type::Void));
|
||||
env.functions.insert("canvas_unclip".into(), str_fn(vec![], Type::Void));
|
||||
env.functions.insert("canvas_size".into(), str_fn(vec![], Type::Unknown));
|
||||
env.functions.insert("canvas_mouse_pos".into(), str_fn(vec![], Type::Unknown));
|
||||
env.functions.insert("canvas_events".into(), str_fn(vec![], s.clone()));
|
||||
env.functions.insert("canvas_swap".into(), str_fn(vec![], Type::Void));
|
||||
env.functions.insert("canvas_run_loop".into(), str_fn(vec![s.clone()], Type::Void));
|
||||
|
||||
// Math
|
||||
for name in &["math_abs","math_floor","math_ceil","math_round","math_sqrt"] {
|
||||
|
||||
Reference in New Issue
Block a user