use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::sync::Arc;
use crate::geometry::{PhysicalSize, Point, Rect, ScaleFactor, Size};
use crate::platform::access::{A11yNode, aria_json};
use crate::platform::event::Ime;
use crate::platform::event::{ButtonState, Event, KeyCode, PointerButton, WindowId};
use crate::platform::input::{css_cursor, web_key, web_modifiers, web_wheel};
use crate::platform::native::SystemLook;
use crate::platform::time::Instant;
use crate::platform::window::{CursorIcon, ImeState, Window, WindowAttributes};
use crate::platform::{ControlFlow, frame, wake};
use crate::render::{Font, Pixmap, Surface};
mod js {
#[link(wasm_import_module = "stipple")]
unsafe extern "C" {
pub fn now() -> f64;
pub fn present(ptr: *const u8, width: u32, height: u32, rects: *const u32, n: usize);
pub fn schedule(ms: f64);
pub fn request_frame();
pub fn set_cursor(ptr: *const u8, len: usize);
pub fn set_title(ptr: *const u8, len: usize);
pub fn clipboard_write(ptr: *const u8, len: usize);
pub fn open_file(id: u32, accept: *const u8, accept_len: usize);
pub fn save_file(name: *const u8, name_len: usize, data: *const u8, data_len: usize);
pub fn log(level: u32, ptr: *const u8, len: usize);
pub fn set_a11y(ptr: *const u8, len: usize);
pub fn set_ime(active: u32, x: f64, y: f64, width: f64, height: f64);
pub fn set_text_input(active: u32);
pub fn open_url(ptr: *const u8, len: usize);
pub fn notify(title: *const u8, title_len: usize, body: *const u8, body_len: usize);
}
}
pub(crate) fn now_ms() -> f64 {
unsafe { js::now() }
}
pub(crate) fn log(msg: &str, error: bool) {
unsafe { js::log(u32::from(error), msg.as_ptr(), msg.len()) }
}
pub(crate) fn clipboard_write(text: &str) {
unsafe { js::clipboard_write(text.as_ptr(), text.len()) }
}
pub(crate) fn clipboard_read() -> Option<String> {
HOST.with(|h| h.pasted.borrow().clone())
}
pub(crate) fn open_file(id: u32, accept: &str) {
unsafe { js::open_file(id, accept.as_ptr(), accept.len()) }
}
pub(crate) fn save_file(name: &str, data: &[u8]) {
unsafe { js::save_file(name.as_ptr(), name.len(), data.as_ptr(), data.len()) }
}
pub(crate) fn open_url(url: &str) {
unsafe { js::open_url(url.as_ptr(), url.len()) }
}
pub(crate) fn notify(title: &str, body: &str) {
unsafe { js::notify(title.as_ptr(), title.len(), body.as_ptr(), body.len()) }
}
type Handler = Box<dyn FnMut(Event, &dyn Window) -> ControlFlow>;
struct Host {
handler: RefCell<Option<Handler>>,
queue: RefCell<VecDeque<Event>>,
busy: Cell<bool>,
css: Cell<(f64, f64)>,
scale: Cell<f64>,
pasted: RefCell<Option<String>>,
cursor: Cell<CursorIcon>,
font: RefCell<Option<Font>>,
a11y: RefCell<Option<A11yNode>>,
text_input: Cell<bool>,
}
thread_local! {
static HOST: Host = const {
Host {
handler: RefCell::new(None),
queue: RefCell::new(VecDeque::new()),
busy: Cell::new(false),
css: Cell::new((0.0, 0.0)),
scale: Cell::new(1.0),
pasted: RefCell::new(None),
cursor: Cell::new(CursorIcon::Default),
font: RefCell::new(None),
a11y: RefCell::new(None),
text_input: Cell::new(false),
}
};
}
pub(crate) fn run<H>(attrs: WindowAttributes, handler: H)
where
H: FnMut(Event, &dyn Window) -> ControlFlow + 'static,
{
HOST.with(|h| {
if h.css.get().0 <= 0.0 {
let s = attrs.logical_size;
h.css.set((s.width, s.height));
}
*h.handler.borrow_mut() = Some(Box::new(handler));
});
if attrs.title != WindowAttributes::default().title {
WebWindow.set_title(&attrs.title);
}
wake::install(Arc::new(rearm), Some(Arc::new(|_| rearm())));
frame::install(Arc::new(|| unsafe { js::request_frame() }), None);
deliver(Event::RedrawRequested);
}
fn deliver(event: Event) {
HOST.with(|h| h.queue.borrow_mut().push_back(event));
drain();
}
fn drain() {
if HOST.with(|h| h.busy.replace(true)) {
return;
}
while let Some(event) = HOST.with(|h| h.queue.borrow_mut().pop_front()) {
let Some(mut handler) = HOST.with(|h| h.handler.borrow_mut().take()) else {
HOST.with(|h| h.queue.borrow_mut().clear());
break;
};
if handler(event, &WebWindow) != ControlFlow::Exit {
HOST.with(|h| *h.handler.borrow_mut() = Some(handler));
}
}
HOST.with(|h| h.busy.set(false));
rearm();
}
fn rearm() {
let ms = match wake::timeout(Instant::now()) {
Some(t) => t.as_secs_f64() * 1000.0,
None => -1.0,
};
unsafe { js::schedule(ms) }
}
struct WebWindow;
fn physical(css: (f64, f64), scale: f64) -> PhysicalSize {
PhysicalSize::new(
((css.0 * scale).round() as u32).max(1),
((css.1 * scale).round() as u32).max(1),
)
}
impl Window for WebWindow {
fn id(&self) -> WindowId {
WindowId(1)
}
fn inner_size(&self) -> PhysicalSize {
HOST.with(|h| physical(h.css.get(), h.scale.get()))
}
fn scale_factor(&self) -> ScaleFactor {
ScaleFactor::new(HOST.with(|h| h.scale.get()))
}
fn request_redraw(&self) {
deliver(Event::RedrawRequested);
}
fn set_title(&self, title: &str) {
unsafe { js::set_title(title.as_ptr(), title.len()) }
}
fn create_surface(&self) -> Box<dyn Surface> {
Box::new(WebSurface {
size: self.inner_size(),
})
}
fn clipboard(&self) -> Option<String> {
clipboard_read()
}
fn set_clipboard(&self, text: &str) {
clipboard_write(text);
}
fn set_cursor(&self, cursor: CursorIcon) {
if HOST.with(|h| h.cursor.replace(cursor)) != cursor {
let css = css_cursor(cursor);
unsafe { js::set_cursor(css.as_ptr(), css.len()) }
}
}
fn set_accessibility_tree(&self, root: &A11yNode) {
let changed = HOST.with(|h| {
let mut sent = h.a11y.borrow_mut();
if sent.as_ref() == Some(root) {
return false;
}
*sent = Some(root.clone());
true
});
if changed {
let json = aria_json(root);
unsafe { js::set_a11y(json.as_ptr(), json.len()) }
}
}
fn set_ime(&self, state: Option<&ImeState>) {
match state {
Some(s) => {
let r = s.cursor_area;
unsafe { js::set_ime(1, r.min_x(), r.min_y(), r.width(), r.height()) }
}
None => unsafe { js::set_ime(0, 0.0, 0.0, 0.0, 0.0) },
}
}
fn set_text_input_active(&self, active: bool) {
if HOST.with(|h| h.text_input.replace(active)) != active {
unsafe { js::set_text_input(u32::from(active)) }
}
}
}
struct WebSurface {
size: PhysicalSize,
}
impl Surface for WebSurface {
fn resize(&mut self, size: PhysicalSize) {
self.size = size;
}
fn size(&self) -> PhysicalSize {
self.size
}
fn present(&mut self, pixmap: &Pixmap, damage: &[Rect]) {
let size = pixmap.size();
let rects = damage_rects(damage, size);
if !damage.is_empty() && rects.is_empty() {
return; }
unsafe {
js::present(
pixmap.as_bytes().as_ptr(),
size.width,
size.height,
rects.as_ptr(),
rects.len() / 4,
)
}
}
}
fn damage_rects(damage: &[Rect], size: PhysicalSize) -> Vec<u32> {
let mut out = Vec::with_capacity(damage.len() * 4);
for r in damage {
let x0 = r.min_x().floor().max(0.0) as u32;
let y0 = r.min_y().floor().max(0.0) as u32;
let x1 = (r.max_x().ceil().max(0.0) as u32).min(size.width);
let y1 = (r.max_y().ceil().max(0.0) as u32).min(size.height);
if x1 > x0 && y1 > y0 {
out.extend([x0, y0, x1 - x0, y1 - y0]);
}
}
out
}
#[doc(hidden)]
pub fn alloc(len: usize) -> *mut u8 {
Box::into_raw(vec![0u8; len].into_boxed_slice()).cast()
}
#[doc(hidden)]
pub unsafe fn take_bytes(ptr: *mut u8, len: usize) -> Vec<u8> {
if ptr.is_null() || len == 0 {
return Vec::new();
}
unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) }.into_vec()
}
#[doc(hidden)]
pub unsafe fn take_string(ptr: *mut u8, len: usize) -> String {
let bytes = unsafe { take_bytes(ptr, len) };
String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
#[doc(hidden)]
pub fn set_font(bytes: Vec<u8>) {
match Font::from_bytes(bytes) {
Ok(font) => HOST.with(|h| *h.font.borrow_mut() = Some(font)),
Err(_) => log("stipple: the font could not be read", true),
}
}
#[doc(hidden)]
pub fn start<S: 'static>(
width: f64,
height: f64,
scale: f64,
make: impl FnOnce() -> crate::App<S>,
) {
std::panic::set_hook(Box::new(|info| log(&format!("stipple: {info}"), true)));
HOST.with(|h| {
h.css.set((width.max(1.0), height.max(1.0)));
h.scale.set(ScaleFactor::new(scale).get());
});
let mut app = make();
if app.font.is_none()
&& let Some(font) = HOST.with(|h| h.font.borrow_mut().take())
{
app = app.font(font);
}
app = app.logical_size(Size::new(width.max(1.0), height.max(1.0)));
app.run();
}
#[doc(hidden)]
pub fn resize(width: f64, height: f64, scale: f64) {
let css = (width.max(1.0), height.max(1.0));
let scale = ScaleFactor::new(scale).get();
let (old_css, old_scale) = HOST.with(|h| (h.css.replace(css), h.scale.replace(scale)));
if scale != old_scale {
deliver(Event::ScaleFactorChanged(ScaleFactor::new(scale)));
}
if css != old_css || scale != old_scale {
deliver(Event::Resized(physical(css, scale)));
}
}
#[doc(hidden)]
pub fn pointer(kind: u32, button: u32, x: f64, y: f64, mods: u32) {
let position = Point::new(x, y);
let button = match button {
0 => PointerButton::Left,
1 => PointerButton::Middle,
2 => PointerButton::Right,
b => PointerButton::Other(b as u16),
};
let state = match kind {
0 => return deliver(Event::PointerMoved { position }),
1 => ButtonState::Pressed,
2 => ButtonState::Released,
_ => return deliver(Event::PointerLeft),
};
crate::platform::set_pointer_modifiers(web_modifiers(mods));
deliver(Event::PointerButton {
button,
state,
position,
});
}
#[doc(hidden)]
pub fn wheel(dx: f64, dy: f64, mode: u32) {
let page = HOST.with(|h| h.css.get().1);
deliver(Event::Scroll {
delta: web_wheel(dx, dy, mode, page),
});
}
#[doc(hidden)]
pub fn key(key: &str, mods: u32) -> bool {
let modifiers = web_modifiers(mods);
let Some(code) = web_key(key) else {
return false;
};
let command = modifiers.ctrl || modifiers.meta;
let shortcut = match code {
KeyCode::Char(c) if command => Some(c.to_ascii_lowercase()),
_ => None,
};
if shortcut == Some('v') {
return false; }
let text = match code {
KeyCode::Char(c) if !command || (modifiers.ctrl && modifiers.alt) => Some(c.to_string()),
KeyCode::Space if !command => Some(" ".to_string()),
_ => None,
};
deliver(Event::Key {
code,
state: ButtonState::Pressed,
modifiers,
});
if let Some(text) = text {
deliver(Event::Text(text));
}
match shortcut {
Some(c) => matches!(c, 'a' | 'z' | 'y'),
None => !matches!(
code,
KeyCode::F1
| KeyCode::F2
| KeyCode::F3
| KeyCode::F4
| KeyCode::F5
| KeyCode::F6
| KeyCode::F7
| KeyCode::F8
| KeyCode::F9
| KeyCode::F10
| KeyCode::F11
| KeyCode::F12
),
}
}
#[doc(hidden)]
pub fn paste(text: String) {
HOST.with(|h| *h.pasted.borrow_mut() = Some(text));
deliver(Event::Key {
code: KeyCode::Char('v'),
state: ButtonState::Pressed,
modifiers: crate::platform::Modifiers {
ctrl: true,
..Default::default()
},
});
}
#[doc(hidden)]
pub fn ime(kind: u32, text: String) {
let event = if kind == 0 {
let end = text.len();
Ime::Preedit {
text,
cursor: Some((end, end)),
}
} else {
Ime::Commit(text)
};
deliver(Event::Ime(event));
}
#[doc(hidden)]
pub fn color_scheme(dark: bool, accent: i32) {
let accent = u32::try_from(accent)
.ok()
.map(|c| crate::render::Color::rgb((c >> 16) as u8, (c >> 8) as u8, c as u8));
let changed = crate::platform::native::set_web_look(SystemLook { dark, accent });
if changed && HOST.with(|h| h.handler.borrow().is_some()) {
deliver(Event::ThemeChanged);
}
}
#[doc(hidden)]
pub fn reduced_motion(reduced: bool) {
let changed = crate::platform::native::set_web_reduced_motion(reduced);
if changed && HOST.with(|h| h.handler.borrow().is_some()) {
deliver(Event::ThemeChanged);
}
}
#[doc(hidden)]
pub fn focus(focused: bool) {
deliver(Event::FocusChanged(focused));
}
#[doc(hidden)]
pub fn on_wake() {
if wake::take(Instant::now()) {
deliver(Event::Wake);
} else {
drain();
}
}
#[doc(hidden)]
pub fn on_frame() {
frame::tick();
on_wake();
}
#[doc(hidden)]
pub fn file_picked(id: u32, file: Option<(String, Vec<u8>)>) {
crate::platform::dialog::web_picked(id, file);
drain();
}