use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::num::NonZeroU32;
use std::rc::Rc;
use std::time::Duration;
use fontdue::layout::{CoordinateSystem, GlyphRasterConfig, Layout, LayoutSettings, TextStyle};
use tiny_skia::{
Color, ColorU8, FillRule, FilterQuality, Paint, Path, PathBuilder, Pixmap, PixmapPaint,
PremultipliedColorU8, Rect, Stroke, Transform,
};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::{Key, ModifiersState, PhysicalKey};
use winit::platform::pump_events::EventLoopExtPumpEvents;
use winit::window::{Window, WindowId};
use crate::vm::libs::args::array as new_table;
use crate::vm::machine::VM;
use crate::vm::value::{NativeDef, StrRef, Value, VmTable};
pub(crate) const NATIVES: &[NativeDef] = &[
NativeDef {
name: "__ui_available",
func: builtin_available,
},
NativeDef {
name: "__ui_open",
func: builtin_open,
},
NativeDef {
name: "__ui_close",
func: builtin_close,
},
NativeDef {
name: "__ui_set_title",
func: builtin_set_title,
},
NativeDef {
name: "__ui_set_size",
func: builtin_set_size,
},
NativeDef {
name: "__ui_size",
func: builtin_size,
},
NativeDef {
name: "__ui_poll",
func: builtin_poll,
},
NativeDef {
name: "__ui_clear",
func: builtin_clear,
},
NativeDef {
name: "__ui_fill_rect",
func: builtin_fill_rect,
},
NativeDef {
name: "__ui_stroke_rect",
func: builtin_stroke_rect,
},
NativeDef {
name: "__ui_line",
func: builtin_line,
},
NativeDef {
name: "__ui_circle",
func: builtin_circle,
},
NativeDef {
name: "__ui_polygon",
func: builtin_polygon,
},
NativeDef {
name: "__ui_text",
func: builtin_text,
},
NativeDef {
name: "__ui_measure_text",
func: builtin_measure_text,
},
NativeDef {
name: "__ui_load_image",
func: builtin_load_image,
},
NativeDef {
name: "__ui_free_image",
func: builtin_free_image,
},
NativeDef {
name: "__ui_draw_image",
func: builtin_draw_image,
},
NativeDef {
name: "__ui_load_font",
func: builtin_load_font,
},
NativeDef {
name: "__ui_free_font",
func: builtin_free_font,
},
NativeDef {
name: "__ui_present",
func: builtin_present,
},
];
const DEFAULT_FONT: &[u8] = include_bytes!("../../../lib/fonts/NotoSans-Regular.ttf");
const DEFAULT_FONT_ID: u64 = 0;
const GLYPH_CACHE_LIMIT: usize = 4096;
const OPEN_ATTEMPTS: usize = 8;
struct Win {
window: Rc<Window>,
surface: softbuffer::Surface<Rc<Window>, Rc<Window>>,
pixmap: Pixmap,
scale: f64,
modifiers: ModifiersState,
cursor: (f64, f64),
}
struct FontEntry {
font: fontdue::Font,
glyphs: HashMap<GlyphRasterConfig, Rc<(fontdue::Metrics, Vec<u8>)>>,
}
struct OpenRequest {
title: String,
width: f64,
height: f64,
resizable: bool,
visible: bool,
}
thread_local! {
static EVENT_LOOP: RefCell<Option<Result<EventLoop<()>, String>>> = const { RefCell::new(None) };
static WINDOWS: RefCell<HashMap<u64, Win>> = RefCell::new(HashMap::new());
static WINDOW_IDS: RefCell<HashMap<WindowId, u64>> = RefCell::new(HashMap::new());
static IMAGES: RefCell<HashMap<u64, Pixmap>> = RefCell::new(HashMap::new());
static FONTS: RefCell<HashMap<u64, FontEntry>> = RefCell::new(HashMap::new());
static EVENTS: RefCell<Vec<Value>> = const { RefCell::new(Vec::new()) };
static PENDING_OPEN: RefCell<Option<OpenRequest>> = const { RefCell::new(None) };
static OPENED: RefCell<Option<Result<(u64, f64), String>>> = const { RefCell::new(None) };
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
}
fn next_id() -> u64 {
NEXT_ID.with(|id| {
let value = id.get();
id.set(value + 1);
value
})
}
fn string_arg(args: &[Value], index: usize, name: &str) -> Result<String, String> {
match args.get(index) {
Some(Value::String(value)) => Ok(value.to_string()),
Some(value) => Err(format!(
"{} must be a string, got {}",
name,
value.type_name()
)),
None => Err(format!("{} must be provided", name)),
}
}
fn bool_arg(args: &[Value], index: usize, name: &str, default: bool) -> Result<bool, String> {
match args.get(index) {
Some(Value::Bool(value)) => Ok(*value),
None | Some(Value::Nil) => Ok(default),
Some(value) => Err(format!(
"{} must be a boolean, got {}",
name,
value.type_name()
)),
}
}
fn float_arg(args: &[Value], index: usize, name: &str) -> Result<f64, String> {
let value = args.get(index).unwrap_or(&Value::Nil);
if matches!(value, Value::Nil) {
return Err(format!("{} must be provided", name));
}
let number =
crate::vm::libs::args::number(value).map_err(|_| format!("{} must be a number", name))?;
if !number.is_finite() {
return Err(format!("{} must be a finite number", name));
}
Ok(number)
}
fn optional_float_arg(
args: &[Value],
index: usize,
name: &str,
default: f64,
) -> Result<f64, String> {
match args.get(index) {
None | Some(Value::Nil) => Ok(default),
_ => float_arg(args, index, name),
}
}
fn integer_arg(args: &[Value], index: usize, name: &str) -> Result<u64, String> {
let number = float_arg(args, index, name)?;
if number < 0.0 || number.fract() != 0.0 {
return Err(format!("{} must be a non-negative integer", name));
}
Ok(number as u64)
}
fn color_arg(args: &[Value], index: usize, name: &str) -> Result<(u8, u8, u8, u8), String> {
let packed = integer_arg(args, index, name)?;
if packed > u32::MAX as u64 {
return Err(format!("{} is out of range", name));
}
Ok(unpack_color(packed as u32))
}
fn unpack_color(packed: u32) -> (u8, u8, u8, u8) {
(
(packed >> 24) as u8,
(packed >> 16) as u8,
(packed >> 8) as u8,
packed as u8,
)
}
fn timeout_arg(args: &[Value], index: usize, name: &str) -> Result<Duration, String> {
let seconds = optional_float_arg(args, index, name, 0.0)?;
if seconds < 0.0 {
return Err(format!("{} must be a non-negative number of seconds", name));
}
Ok(Duration::from_secs_f64(seconds))
}
fn points_arg(args: &[Value], index: usize, name: &str) -> Result<Vec<(f32, f32)>, String> {
let numbers = match args.get(index) {
Some(Value::Table(table)) => table
.borrow()
.array
.iter()
.map(|value| {
crate::vm::libs::args::number(value)
.ok()
.filter(|number| number.is_finite())
.ok_or_else(|| format!("{} must hold finite numbers", name))
})
.collect::<Result<Vec<f64>, String>>()?,
Some(value) => {
return Err(format!(
"{} must be a table, got {}",
name,
value.type_name()
));
}
None => return Err(format!("{} must be provided", name)),
};
if numbers.len() % 2 != 0 {
return Err(format!("{} must hold x, y pairs", name));
}
Ok(numbers
.chunks(2)
.map(|pair| (pair[0] as f32, pair[1] as f32))
.collect())
}
fn record(fields: Vec<(&str, Value)>) -> Value {
let mut table = VmTable::new();
for (name, value) in fields {
table.fields.insert(StrRef::from(name), value);
}
Value::Table(Rc::new(RefCell::new(table)))
}
fn integer(value: u64) -> Value {
Value::from_u64(value)
}
fn text(value: &str) -> Value {
Value::str(value)
}
fn with_event_loop<T>(body: impl FnOnce(&mut EventLoop<()>) -> T) -> Result<T, String> {
EVENT_LOOP.with(|cell| {
let mut slot = cell.borrow_mut();
let entry = slot.get_or_insert_with(create_event_loop);
match entry {
Ok(event_loop) => Ok(body(event_loop)),
Err(err) => Err(err.clone()),
}
})
}
fn create_event_loop() -> Result<EventLoop<()>, String> {
let mut builder = EventLoop::<()>::with_user_event();
#[cfg(target_os = "linux")]
{
winit::platform::x11::EventLoopBuilderExtX11::with_any_thread(&mut builder, true);
winit::platform::wayland::EventLoopBuilderExtWayland::with_any_thread(&mut builder, true);
}
#[cfg(target_os = "windows")]
{
winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread(&mut builder, true);
}
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| builder.build()));
match built {
Ok(Ok(event_loop)) => Ok(event_loop),
Ok(Err(err)) => Err(format!(
"no display available: {}",
tidy_os_error(&err.to_string())
)),
Err(payload) => {
let message = payload
.downcast_ref::<String>()
.cloned()
.or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown error".to_string());
Err(format!("cannot start the UI event loop: {}", message))
}
}
}
fn tidy_os_error(message: &str) -> String {
match message.strip_prefix("os error at ") {
Some(rest) => match rest.split_once(": ") {
Some((_, detail)) => detail.to_string(),
None => rest.to_string(),
},
None => message.to_string(),
}
}
fn pump(timeout: Duration) -> Result<(), String> {
with_event_loop(|event_loop| {
event_loop.pump_app_events(Some(timeout), &mut App);
})
}
struct App;
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
create_pending(event_loop);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
create_pending(event_loop);
}
fn window_event(&mut self, _: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
let Some(id) = WINDOW_IDS.with(|ids| ids.borrow().get(&window_id).copied()) else {
return;
};
WINDOWS.with(|windows| {
let mut windows = windows.borrow_mut();
let Some(win) = windows.get_mut(&id) else {
return;
};
if let Some(event) = translate_event(id, win, event) {
EVENTS.with(|events| events.borrow_mut().push(event));
}
});
}
}
fn create_pending(event_loop: &ActiveEventLoop) {
let Some(request) = PENDING_OPEN.with(|pending| pending.borrow_mut().take()) else {
return;
};
let result = create_window(event_loop, request);
OPENED.with(|opened| *opened.borrow_mut() = Some(result));
}
fn create_window(event_loop: &ActiveEventLoop, request: OpenRequest) -> Result<(u64, f64), String> {
let attributes = Window::default_attributes()
.with_title(&request.title)
.with_inner_size(LogicalSize::new(request.width, request.height))
.with_resizable(request.resizable)
.with_visible(request.visible);
let window = Rc::new(
event_loop
.create_window(attributes)
.map_err(|err| format!("cannot create window: {}", tidy_os_error(&err.to_string())))?,
);
let context = softbuffer::Context::new(window.clone())
.map_err(|err| format!("cannot create window: {}", err))?;
let surface = softbuffer::Surface::new(&context, window.clone())
.map_err(|err| format!("cannot create window: {}", err))?;
let scale = window.scale_factor();
let size = window.inner_size();
let mut pixmap = Pixmap::new(size.width.max(1), size.height.max(1))
.ok_or_else(|| "cannot create window: back buffer too large".to_string())?;
pixmap.fill(Color::WHITE);
let id = next_id();
let mut win = Win {
window: window.clone(),
surface,
pixmap,
scale,
modifiers: ModifiersState::empty(),
cursor: (0.0, 0.0),
};
let _ = blit(&mut win);
WINDOW_IDS.with(|ids| ids.borrow_mut().insert(window.id(), id));
WINDOWS.with(|windows| windows.borrow_mut().insert(id, win));
Ok((id, scale))
}
fn translate_event(id: u64, win: &mut Win, event: WindowEvent) -> Option<Value> {
let kind = |kind: &str, mut fields: Vec<(&str, Value)>| {
fields.insert(0, ("kind", text(kind)));
fields.insert(1, ("window", integer(id)));
Some(record(fields))
};
let logical = |win: &Win| {
let size = win.window.inner_size().to_logical::<f64>(win.scale);
(size.width, size.height)
};
match event {
WindowEvent::CloseRequested => kind("close", vec![]),
WindowEvent::Resized(size) => {
if let Some(pixmap) = Pixmap::new(size.width.max(1), size.height.max(1)) {
let mut pixmap = pixmap;
pixmap.fill(Color::WHITE);
win.pixmap = pixmap;
}
let (width, height) = logical(win);
kind(
"resize",
vec![
("width", Value::Float(width)),
("height", Value::Float(height)),
],
)
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
win.scale = scale_factor;
None
}
WindowEvent::Focused(focused) => kind(if focused { "focus" } else { "blur" }, vec![]),
WindowEvent::RedrawRequested => {
let _ = blit(win);
kind("redraw", vec![])
}
WindowEvent::ModifiersChanged(modifiers) => {
win.modifiers = modifiers.state();
None
}
WindowEvent::KeyboardInput { event, .. } => {
let name = match &event.logical_key {
Key::Named(named) => format!("{:?}", named).to_lowercase(),
Key::Character(s) => s.to_lowercase(),
Key::Unidentified(_) => "unidentified".to_string(),
Key::Dead(_) => "dead".to_string(),
};
let code = match event.physical_key {
PhysicalKey::Code(code) => format!("{:?}", code),
PhysicalKey::Unidentified(_) => "Unidentified".to_string(),
};
let typed = event
.text
.as_ref()
.map(|s| s.to_string())
.filter(|s| !s.chars().all(char::is_control));
let mods = win.modifiers;
kind(
match event.state {
ElementState::Pressed => "keydown",
ElementState::Released => "keyup",
},
vec![
("key", Value::string(name)),
("code", Value::string(code)),
("repeat", Value::Bool(event.repeat)),
("text", typed.map(Value::string).unwrap_or(Value::Nil)),
(
"mods",
record(vec![
("shift", Value::Bool(mods.shift_key())),
("ctrl", Value::Bool(mods.control_key())),
("alt", Value::Bool(mods.alt_key())),
("meta", Value::Bool(mods.super_key())),
]),
),
],
)
}
WindowEvent::CursorMoved { position, .. } => {
let position = position.to_logical::<f64>(win.scale);
win.cursor = (position.x, position.y);
kind(
"mousemove",
vec![
("x", Value::Float(position.x)),
("y", Value::Float(position.y)),
],
)
}
WindowEvent::CursorEntered { .. } => kind("mouseenter", vec![]),
WindowEvent::CursorLeft { .. } => kind("mouseleave", vec![]),
WindowEvent::MouseInput { state, button, .. } => {
let button = match button {
MouseButton::Left => "left".to_string(),
MouseButton::Right => "right".to_string(),
MouseButton::Middle => "middle".to_string(),
MouseButton::Back => "back".to_string(),
MouseButton::Forward => "forward".to_string(),
MouseButton::Other(n) => format!("button{}", n),
};
kind(
match state {
ElementState::Pressed => "mousedown",
ElementState::Released => "mouseup",
},
vec![
("button", Value::string(button)),
("x", Value::Float(win.cursor.0)),
("y", Value::Float(win.cursor.1)),
],
)
}
WindowEvent::MouseWheel { delta, .. } => {
let (dx, dy, unit) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x as f64, y as f64, "lines"),
MouseScrollDelta::PixelDelta(position) => {
let position = position.to_logical::<f64>(win.scale);
(position.x, position.y, "pixels")
}
};
kind(
"scroll",
vec![
("dx", Value::Float(dx)),
("dy", Value::Float(dy)),
("unit", text(unit)),
("x", Value::Float(win.cursor.0)),
("y", Value::Float(win.cursor.1)),
],
)
}
_ => None,
}
}
fn with_window<T>(id: u64, body: impl FnOnce(&mut Win) -> Result<T, String>) -> Result<T, String> {
WINDOWS.with(|windows| {
let mut windows = windows.borrow_mut();
let win = windows
.get_mut(&id)
.ok_or_else(|| "window is closed".to_string())?;
body(win)
})
}
fn blit(win: &mut Win) -> Result<(), String> {
let (width, height) = (win.pixmap.width(), win.pixmap.height());
let (Some(w), Some(h)) = (NonZeroU32::new(width), NonZeroU32::new(height)) else {
return Ok(());
};
win.surface
.resize(w, h)
.map_err(|err| format!("cannot present window: {}", err))?;
let mut buffer = win
.surface
.buffer_mut()
.map_err(|err| format!("cannot present window: {}", err))?;
pack_pixels(win.pixmap.pixels(), &mut buffer);
buffer
.present()
.map_err(|err| format!("cannot present window: {}", err))
}
fn pack_pixels(pixels: &[PremultipliedColorU8], out: &mut [u32]) {
for (pixel, word) in pixels.iter().zip(out.iter_mut()) {
*word = (pixel.red() as u32) << 16 | (pixel.green() as u32) << 8 | pixel.blue() as u32;
}
}
fn paint(color: (u8, u8, u8, u8)) -> Paint<'static> {
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
paint.anti_alias = true;
paint
}
fn stroke(width: f64) -> Stroke {
Stroke {
width: width as f32,
..Stroke::default()
}
}
fn rounded_rect(x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<Path> {
let r = radius.min(w / 2.0).min(h / 2.0).max(0.0);
if r == 0.0 {
return PathBuilder::from_rect(Rect::from_xywh(x, y, w, h)?).into();
}
let k = r * 0.552_284_8;
let (right, bottom) = (x + w, y + h);
let mut pb = PathBuilder::new();
pb.move_to(x + r, y);
pb.line_to(right - r, y);
pb.cubic_to(right - r + k, y, right, y + r - k, right, y + r);
pb.line_to(right, bottom - r);
pb.cubic_to(
right,
bottom - r + k,
right - r + k,
bottom,
right - r,
bottom,
);
pb.line_to(x + r, bottom);
pb.cubic_to(x + r - k, bottom, x, bottom - r + k, x, bottom - r);
pb.line_to(x, y + r);
pb.cubic_to(x, y + r - k, x + r - k, y, x + r, y);
pb.close();
pb.finish()
}
fn polygon_path(points: &[(f32, f32)]) -> Option<Path> {
let mut pb = PathBuilder::new();
let (first, rest) = points.split_first()?;
pb.move_to(first.0, first.1);
for (x, y) in rest {
pb.line_to(*x, *y);
}
pb.close();
pb.finish()
}
fn rect_args(args: &[Value]) -> Result<(u64, f32, f32, f32, f32), String> {
let id = integer_arg(args, 0, "window")?;
let x = float_arg(args, 1, "x")? as f32;
let y = float_arg(args, 2, "y")? as f32;
let w = float_arg(args, 3, "width")? as f32;
let h = float_arg(args, 4, "height")? as f32;
if w < 0.0 || h < 0.0 {
return Err("width and height must be non-negative".to_string());
}
Ok((id, x, y, w, h))
}
fn load_font(bytes: Vec<u8>) -> Result<FontEntry, String> {
let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
.map_err(|err| format!("cannot load font: {}", err))?;
Ok(FontEntry {
font,
glyphs: HashMap::new(),
})
}
fn with_font<T>(
id: u64,
body: impl FnOnce(&mut FontEntry) -> Result<T, String>,
) -> Result<T, String> {
FONTS.with(|fonts| {
let mut fonts = fonts.borrow_mut();
if id == DEFAULT_FONT_ID && !fonts.contains_key(&id) {
fonts.insert(id, load_font(DEFAULT_FONT.to_vec())?);
}
let entry = fonts
.get_mut(&id)
.ok_or_else(|| "font was freed".to_string())?;
body(entry)
})
}
fn layout_text(font: &fontdue::Font, text: &str, px: f32) -> (Layout, f32, f32) {
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.reset(&LayoutSettings::default());
layout.append(&[font], &TextStyle::new(text, px, 0));
let width = layout
.glyphs()
.iter()
.map(|glyph| glyph.x + glyph.width as f32)
.fold(0.0_f32, f32::max);
let height = layout.height();
(layout, width, height)
}
fn blend_mask(
pixmap: &mut Pixmap,
x0: i32,
y0: i32,
width: usize,
height: usize,
mask: &[u8],
color: (u8, u8, u8, u8),
) {
let (pw, ph) = (pixmap.width() as i32, pixmap.height() as i32);
let pixels = pixmap.pixels_mut();
let (r, g, b, a) = (
color.0 as u32,
color.1 as u32,
color.2 as u32,
color.3 as u32,
);
for row in 0..height {
let y = y0 + row as i32;
if y < 0 || y >= ph {
continue;
}
for column in 0..width {
let x = x0 + column as i32;
if x < 0 || x >= pw {
continue;
}
let coverage = mask[row * width + column] as u32;
if coverage == 0 {
continue;
}
let alpha = coverage * a / 255;
if alpha == 0 {
continue;
}
let index = (y * pw + x) as usize;
let dst = pixels[index];
let inverse = 255 - alpha;
let na = alpha + dst.alpha() as u32 * inverse / 255;
let nr = (r * alpha / 255 + dst.red() as u32 * inverse / 255).min(na);
let ng = (g * alpha / 255 + dst.green() as u32 * inverse / 255).min(na);
let nb = (b * alpha / 255 + dst.blue() as u32 * inverse / 255).min(na);
if let Some(blended) =
PremultipliedColorU8::from_rgba(nr as u8, ng as u8, nb as u8, na as u8)
{
pixels[index] = blended;
}
}
}
}
fn draw_text(
pixmap: &mut Pixmap,
entry: &mut FontEntry,
text: &str,
px: f32,
x: f32,
y: f32,
color: (u8, u8, u8, u8),
) -> f32 {
let (layout, width, _) = layout_text(&entry.font, text, px);
if entry.glyphs.len() > GLYPH_CACHE_LIMIT {
entry.glyphs.clear();
}
for glyph in layout.glyphs() {
if !glyph.char_data.rasterize() || glyph.width == 0 {
continue;
}
let font = &entry.font;
let rasterized = entry
.glyphs
.entry(glyph.key)
.or_insert_with(|| Rc::new(font.rasterize_config(glyph.key)))
.clone();
let (metrics, bitmap) = &*rasterized;
blend_mask(
pixmap,
(x + glyph.x).round() as i32,
(y + glyph.y).round() as i32,
metrics.width,
metrics.height,
bitmap,
color,
);
}
width
}
fn decode_png(path: &str) -> Result<Pixmap, String> {
let file = File::open(path).map_err(|err| format!("cannot open {}: {}", path, err))?;
let mut decoder = png::Decoder::new(BufReader::new(file));
decoder.set_transformations(png::Transformations::normalize_to_color8());
let mut reader = decoder
.read_info()
.map_err(|err| format!("cannot decode {}: {}", path, err))?;
let size = reader
.output_buffer_size()
.ok_or_else(|| format!("cannot decode {}: image too large", path))?;
let mut buffer = vec![0; size];
let info = reader
.next_frame(&mut buffer)
.map_err(|err| format!("cannot decode {}: {}", path, err))?;
let bytes = &buffer[..info.buffer_size()];
let rgba: Vec<u8> = match info.color_type {
png::ColorType::Rgba => bytes.to_vec(),
png::ColorType::Rgb => bytes
.chunks(3)
.flat_map(|p| [p[0], p[1], p[2], 255])
.collect(),
png::ColorType::Grayscale => bytes.iter().flat_map(|&g| [g, g, g, 255]).collect(),
png::ColorType::GrayscaleAlpha => bytes
.chunks(2)
.flat_map(|p| [p[0], p[0], p[0], p[1]])
.collect(),
png::ColorType::Indexed => {
return Err(format!("cannot decode {}: unexpected indexed output", path));
}
};
rgba_to_pixmap(&rgba, info.width, info.height)
.ok_or_else(|| format!("cannot decode {}: image too large", path))
}
fn rgba_to_pixmap(rgba: &[u8], width: u32, height: u32) -> Option<Pixmap> {
let mut pixmap = Pixmap::new(width, height)?;
for (pixel, source) in pixmap.pixels_mut().iter_mut().zip(rgba.chunks(4)) {
*pixel = ColorU8::from_rgba(source[0], source[1], source[2], source[3]).premultiply();
}
Some(pixmap)
}
fn builtin_available(_vm: &mut VM, _args: &[Value]) -> Result<Vec<Value>, String> {
Ok(vec![Value::Bool(with_event_loop(|_| ()).is_ok())])
}
fn builtin_open(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let request = OpenRequest {
title: string_arg(args, 0, "title")?,
width: float_arg(args, 1, "width")?,
height: float_arg(args, 2, "height")?,
resizable: bool_arg(args, 3, "resizable", true)?,
visible: bool_arg(args, 4, "visible", true)?,
};
if request.width <= 0.0 || request.height <= 0.0 {
return Err("width and height must be positive".to_string());
}
PENDING_OPEN.with(|pending| *pending.borrow_mut() = Some(request));
OPENED.with(|opened| *opened.borrow_mut() = None);
for _ in 0..OPEN_ATTEMPTS {
if let Err(err) = pump(Duration::ZERO) {
PENDING_OPEN.with(|pending| *pending.borrow_mut() = None);
return Err(err);
}
if let Some(result) = OPENED.with(|opened| opened.borrow_mut().take()) {
let (id, scale) = result?;
return Ok(vec![integer(id), Value::Float(scale)]);
}
}
PENDING_OPEN.with(|pending| *pending.borrow_mut() = None);
Err("cannot create window: the event loop did not respond".to_string())
}
fn builtin_close(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let removed = WINDOWS.with(|windows| windows.borrow_mut().remove(&id));
if let Some(win) = removed {
WINDOW_IDS.with(|ids| ids.borrow_mut().remove(&win.window.id()));
drop(win);
let _ = pump(Duration::ZERO);
}
Ok(vec![])
}
fn builtin_set_title(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let title = string_arg(args, 1, "title")?;
with_window(id, |win| {
win.window.set_title(&title);
Ok(())
})?;
Ok(vec![])
}
fn builtin_set_size(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let width = float_arg(args, 1, "width")?;
let height = float_arg(args, 2, "height")?;
if width <= 0.0 || height <= 0.0 {
return Err("width and height must be positive".to_string());
}
with_window(id, |win| {
let _ = win
.window
.request_inner_size(LogicalSize::new(width, height));
Ok(())
})?;
Ok(vec![])
}
fn builtin_size(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
with_window(id, |win| {
let size = win.window.inner_size().to_logical::<f64>(win.scale);
Ok(vec![
Value::Float(size.width),
Value::Float(size.height),
Value::Float(win.scale),
])
})
}
fn builtin_poll(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let timeout = timeout_arg(args, 0, "timeout")?;
pump(timeout)?;
let events = EVENTS.with(|events| std::mem::take(&mut *events.borrow_mut()));
Ok(vec![new_table(events)])
}
fn builtin_clear(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let (r, g, b, a) = color_arg(args, 1, "color")?;
with_window(id, |win| {
win.pixmap.fill(Color::from_rgba8(r, g, b, a));
Ok(())
})?;
Ok(vec![])
}
fn builtin_fill_rect(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let (id, x, y, w, h) = rect_args(args)?;
let color = color_arg(args, 5, "color")?;
let radius = optional_float_arg(args, 6, "radius", 0.0)? as f32;
with_window(id, |win| {
if let Some(path) = rounded_rect(x, y, w, h, radius) {
let transform = Transform::from_scale(win.scale as f32, win.scale as f32);
win.pixmap
.fill_path(&path, &paint(color), FillRule::Winding, transform, None);
}
Ok(())
})?;
Ok(vec![])
}
fn builtin_stroke_rect(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let (id, x, y, w, h) = rect_args(args)?;
let color = color_arg(args, 5, "color")?;
let radius = optional_float_arg(args, 6, "radius", 0.0)? as f32;
let width = optional_float_arg(args, 7, "lineWidth", 1.0)?;
with_window(id, |win| {
if let Some(path) = rounded_rect(x, y, w, h, radius) {
let transform = Transform::from_scale(win.scale as f32, win.scale as f32);
win.pixmap
.stroke_path(&path, &paint(color), &stroke(width), transform, None);
}
Ok(())
})?;
Ok(vec![])
}
fn builtin_line(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let x1 = float_arg(args, 1, "x1")? as f32;
let y1 = float_arg(args, 2, "y1")? as f32;
let x2 = float_arg(args, 3, "x2")? as f32;
let y2 = float_arg(args, 4, "y2")? as f32;
let color = color_arg(args, 5, "color")?;
let width = optional_float_arg(args, 6, "lineWidth", 1.0)?;
with_window(id, |win| {
let mut pb = PathBuilder::new();
pb.move_to(x1, y1);
pb.line_to(x2, y2);
if let Some(path) = pb.finish() {
let transform = Transform::from_scale(win.scale as f32, win.scale as f32);
win.pixmap
.stroke_path(&path, &paint(color), &stroke(width), transform, None);
}
Ok(())
})?;
Ok(vec![])
}
fn builtin_circle(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let cx = float_arg(args, 1, "x")? as f32;
let cy = float_arg(args, 2, "y")? as f32;
let radius = float_arg(args, 3, "radius")? as f32;
let color = color_arg(args, 4, "color")?;
let fill = bool_arg(args, 5, "fill", true)?;
let width = optional_float_arg(args, 6, "lineWidth", 1.0)?;
if radius < 0.0 {
return Err("radius must be non-negative".to_string());
}
with_window(id, |win| {
if let Some(path) = PathBuilder::from_circle(cx, cy, radius) {
let transform = Transform::from_scale(win.scale as f32, win.scale as f32);
if fill {
win.pixmap
.fill_path(&path, &paint(color), FillRule::Winding, transform, None);
} else {
win.pixmap
.stroke_path(&path, &paint(color), &stroke(width), transform, None);
}
}
Ok(())
})?;
Ok(vec![])
}
fn builtin_polygon(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let points = points_arg(args, 1, "points")?;
let color = color_arg(args, 2, "color")?;
let fill = bool_arg(args, 3, "fill", true)?;
let width = optional_float_arg(args, 4, "lineWidth", 1.0)?;
if points.len() < 2 {
return Err("points must hold at least two points".to_string());
}
with_window(id, |win| {
if let Some(path) = polygon_path(&points) {
let transform = Transform::from_scale(win.scale as f32, win.scale as f32);
if fill {
win.pixmap
.fill_path(&path, &paint(color), FillRule::Winding, transform, None);
} else {
win.pixmap
.stroke_path(&path, &paint(color), &stroke(width), transform, None);
}
}
Ok(())
})?;
Ok(vec![])
}
fn builtin_text(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let x = float_arg(args, 1, "x")?;
let y = float_arg(args, 2, "y")?;
let text = string_arg(args, 3, "text")?;
let size = float_arg(args, 4, "size")?;
let color = color_arg(args, 5, "color")?;
let font = match args.get(6) {
None | Some(Value::Nil) => DEFAULT_FONT_ID,
_ => integer_arg(args, 6, "font")?,
};
if size <= 0.0 {
return Err("size must be positive".to_string());
}
let width = with_window(id, |win| {
let scale = win.scale;
with_font(font, |entry| {
Ok(draw_text(
&mut win.pixmap,
entry,
&text,
(size * scale) as f32,
(x * scale) as f32,
(y * scale) as f32,
color,
) as f64
/ scale)
})
})?;
Ok(vec![Value::Float(width)])
}
fn builtin_measure_text(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let text = string_arg(args, 0, "text")?;
let size = float_arg(args, 1, "size")?;
let font = match args.get(2) {
None | Some(Value::Nil) => DEFAULT_FONT_ID,
_ => integer_arg(args, 2, "font")?,
};
if size <= 0.0 {
return Err("size must be positive".to_string());
}
let (width, height) = with_font(font, |entry| {
let (_, width, height) = layout_text(&entry.font, &text, size as f32);
Ok((width, height))
})?;
Ok(vec![
Value::Float(width as f64),
Value::Float(height as f64),
])
}
fn builtin_load_image(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let path = string_arg(args, 0, "path")?;
let pixmap = decode_png(&path)?;
let (width, height) = (pixmap.width(), pixmap.height());
let id = next_id();
IMAGES.with(|images| images.borrow_mut().insert(id, pixmap));
Ok(vec![
integer(id),
integer(width as u64),
integer(height as u64),
])
}
fn builtin_free_image(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "image")?;
IMAGES.with(|images| images.borrow_mut().remove(&id));
Ok(vec![])
}
fn builtin_draw_image(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
let image = integer_arg(args, 1, "image")?;
let x = float_arg(args, 2, "x")? as f32;
let y = float_arg(args, 3, "y")? as f32;
let width = optional_float_arg(args, 4, "width", 0.0)? as f32;
let height = optional_float_arg(args, 5, "height", 0.0)? as f32;
if width < 0.0 || height < 0.0 {
return Err("width and height must be non-negative".to_string());
}
IMAGES.with(|images| {
let images = images.borrow();
let source = images
.get(&image)
.ok_or_else(|| "image was freed".to_string())?;
with_window(id, |win| {
let sx = if width > 0.0 {
width / source.width() as f32
} else {
1.0
};
let sy = if height > 0.0 {
height / source.height() as f32
} else {
1.0
};
let transform = Transform::from_scale(sx, sy)
.post_translate(x, y)
.post_scale(win.scale as f32, win.scale as f32);
let paint = PixmapPaint {
quality: FilterQuality::Bilinear,
..PixmapPaint::default()
};
win.pixmap
.draw_pixmap(0, 0, source.as_ref(), &paint, transform, None);
Ok(())
})
})?;
Ok(vec![])
}
fn builtin_load_font(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let path = string_arg(args, 0, "path")?;
let bytes = std::fs::read(&path).map_err(|err| format!("cannot open {}: {}", path, err))?;
let entry = load_font(bytes)?;
let id = next_id();
FONTS.with(|fonts| fonts.borrow_mut().insert(id, entry));
Ok(vec![integer(id)])
}
fn builtin_free_font(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "font")?;
if id == DEFAULT_FONT_ID {
return Err("the default font cannot be freed".to_string());
}
FONTS.with(|fonts| fonts.borrow_mut().remove(&id));
Ok(vec![])
}
fn builtin_present(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let id = integer_arg(args, 0, "window")?;
with_window(id, blit)?;
Ok(vec![])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colors_unpack_as_rgba() {
assert_eq!(unpack_color(0x11223344), (0x11, 0x22, 0x33, 0x44));
assert_eq!(unpack_color(0xff0000ff), (255, 0, 0, 255));
}
#[test]
fn color_arg_rejects_out_of_range_values() {
let err = color_arg(&[Value::Float(1e12)], 0, "color").unwrap_err();
assert!(err.contains("out of range"), "{}", err);
let err = color_arg(&[Value::Float(-1.0)], 0, "color").unwrap_err();
assert!(err.contains("non-negative"), "{}", err);
}
#[test]
fn packing_drops_alpha_and_orders_channels() {
let pixels = [PremultipliedColorU8::from_rgba(10, 20, 30, 255).unwrap()];
let mut out = [0u32; 1];
pack_pixels(&pixels, &mut out);
assert_eq!(out[0], 0x000a141e);
}
#[test]
fn mask_blending_covers_and_clips() {
let mut pixmap = Pixmap::new(4, 4).unwrap();
pixmap.fill(Color::WHITE);
blend_mask(&mut pixmap, 3, 3, 2, 2, &[255; 4], (255, 0, 0, 255));
let pixels = pixmap.pixels();
assert_eq!(
(pixels[15].red(), pixels[15].green(), pixels[15].blue()),
(255, 0, 0)
);
assert_eq!(
(pixels[0].red(), pixels[0].green(), pixels[0].blue()),
(255, 255, 255)
);
blend_mask(&mut pixmap, 0, 0, 1, 1, &[128], (0, 0, 255, 255));
let blended = pixmap.pixels()[0];
assert!(blended.red() < 255 && blended.red() > 0);
assert_eq!(blended.alpha(), 255);
}
#[test]
fn default_font_renders_glyphs() {
let mut entry = load_font(DEFAULT_FONT.to_vec()).unwrap();
let mut pixmap = Pixmap::new(64, 32).unwrap();
pixmap.fill(Color::WHITE);
let width = draw_text(
&mut pixmap,
&mut entry,
"Ab",
20.0,
2.0,
2.0,
(0, 0, 0, 255),
);
assert!(width > 10.0 && width < 40.0, "unexpected width {}", width);
assert!(pixmap.pixels().iter().any(|p| p.red() < 128));
assert!(!entry.glyphs.is_empty());
let (_, w, h) = layout_text(&entry.font, "line\nline", 16.0);
assert!(h > 24.0, "two lines should be taller than one: {}", h);
assert!(w > 0.0);
}
#[test]
fn os_errors_lose_their_source_location() {
assert_eq!(
tidy_os_error("os error at /x/mod.rs:765: DISPLAY is not set."),
"DISPLAY is not set."
);
assert_eq!(tidy_os_error("plain message"), "plain message");
}
#[test]
fn rounded_rect_paths_are_built() {
assert!(rounded_rect(0.0, 0.0, 10.0, 10.0, 0.0).is_some());
assert!(rounded_rect(0.0, 0.0, 10.0, 10.0, 50.0).is_some());
assert!(rounded_rect(0.0, 0.0, -1.0, 10.0, 0.0).is_none());
}
#[test]
fn rgba_images_are_premultiplied() {
let pixmap = rgba_to_pixmap(&[255, 255, 255, 128, 0, 0, 0, 0], 2, 1).unwrap();
let pixels = pixmap.pixels();
assert_eq!(pixels[0].alpha(), 128);
assert!(pixels[0].red() <= 128);
assert_eq!(pixels[1].alpha(), 0);
}
#[test]
fn points_need_pairs() {
let table = new_table(vec![
Value::Float(1.0),
Value::Float(2.0),
Value::Float(3.0),
]);
let err = points_arg(&[table], 0, "points").unwrap_err();
assert!(err.contains("pairs"), "{}", err);
}
}