use std::{cell::Cell, ffi::CString, os::raw::c_char};
use std::sync::Arc;
use crate::{
App, ContextMenuSource, Cursor, Decoded, Frame, Frames, GesturesMode, Gpu, HistoryOp, Host, KeyKind, Modifiers, MouseButton,
PointerKind, RenderingModeType, Thickness, key_name, skia,
};
thread_local! {
static MAKE: Cell<Option<fn() -> Box<dyn App>>> = const { Cell::new(None) };
}
pub fn run(_title: &str, make: fn() -> Box<dyn App>) {
MAKE.set(Some(make));
}
pub struct WebState {
app: Box<dyn App>,
gpu: Gpu,
surface: skia::Surface,
host: Host,
width: i32,
height: i32,
scale: f32,
request: Option<CString>,
pixels: Vec<skia::Data>,
output: Option<CString>,
cpu: Option<Vec<u8>>,
}
fn surface(gpu: &mut Gpu, cpu: bool, width: i32, height: i32) -> skia::Surface {
let surface = if cpu { skia::surfaces::raster_n32_premul((width, height)) } else { gpu.window_surface(width, height, 0, 8) };
surface.expect("no surface for the canvas")
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_create() -> *mut WebState {
let make = MAKE.get().expect("drawnui::run was not called from main");
let mut host = Host { accessibility_on: true, history_on: true, ..Host::default() };
let mut app = make();
app.init(&mut host);
let mut gpu = Gpu::raster();
let surface = surface(&mut gpu, true, 1, 1);
let (request, pixels, output, cpu) = (None, Vec::new(), None, Some(Vec::new()));
Box::into_raw(Box::new(WebState { app, gpu, surface, host, width: 1, height: 1, scale: 1.0, request, pixels, output, cpu }))
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_font(s: *mut WebState, alias: *const u8, alias_len: usize, url: *const u8, url_len: usize, weight: i32) {
let read = |ptr: *const u8, len: usize| std::str::from_utf8(unsafe { std::slice::from_raw_parts(ptr, len) }).ok();
let (Some(alias), Some(url)) = (read(alias, alias_len), read(url, url_len)) else { return };
let s = state(s);
s.app.page_font(alias, url, weight, &mut s.host);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_gestures_lock(s: *mut WebState) -> i32 {
(state(s).app.gestures() == GesturesMode::Lock) as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_accelerated(s: *mut WebState) -> i32 {
(state(s).app.rendering_mode() == RenderingModeType::Accelerated) as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_attach(s: *mut WebState, width: i32, height: i32, scale: f32, gl: i32) -> i32 {
let s = state(s);
let gpu = if gl != 0 { skia::gpu::gl::Interface::new_native().and_then(Gpu::new_gl) } else { None };
s.cpu = gpu.is_none().then(Vec::new);
s.gpu = gpu.unwrap_or_else(Gpu::raster);
(s.width, s.height, s.scale) = (width, height, scale);
s.surface = surface(&mut s.gpu, s.cpu.is_some(), width, height);
s.cpu.is_none() as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_init(width: i32, height: i32, scale: f32) -> *mut WebState {
let s = dui_create();
dui_attach(s, width, height, scale, 1);
s
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_pixels(s: *mut WebState) -> *const u8 {
state(s).cpu.as_ref().map_or(std::ptr::null(), |p| p.as_ptr())
}
fn state<'a>(state: *mut WebState) -> &'a mut WebState {
unsafe { state.as_mut() }.expect("invalid state pointer")
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_frame(s: *mut WebState, time_ms: f64) -> i32 {
let s = state(s);
let mut frame = Frame {
surface: &mut s.surface,
gpu: &mut s.gpu,
width: s.width as f32,
height: s.height as f32,
scale: s.scale,
time_ms,
host: &mut s.host,
};
let more = s.app.frame(&mut frame);
s.gpu.end_frame(&mut s.surface);
if let Some(pixels) = &mut s.cpu {
let (w, h) = (s.width.max(1), s.height.max(1));
pixels.resize(w as usize * h as usize * 4, 0);
let info = skia::ImageInfo::new((w, h), skia::ColorType::RGBA8888, skia::AlphaType::Unpremul, None);
s.surface.read_pixels(&info, pixels, w as usize * 4, (0, 0));
}
more as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_gpu_restored(s: *mut WebState) {
let s = state(s);
let interface = skia::gpu::gl::Interface::new_native().expect("no WebGL interface");
s.gpu = Gpu::new_gl_after(interface, &mut s.gpu).expect("no GPU context");
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_wake(s: *mut WebState) -> f64 {
state(s).host.wake_ms.take().unwrap_or(-1.0)
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_resize(s: *mut WebState, width: i32, height: i32, scale: f32) {
let s = state(s);
(s.width, s.height, s.scale) = (width, height, scale);
s.surface = surface(&mut s.gpu, s.cpu.is_some(), width, height);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_pointer(s: *mut WebState, kind: i32, button: i32, x: f32, y: f32, time_ms: f64) {
let kind = match kind {
0 => PointerKind::Down,
1 => PointerKind::Move,
2 => PointerKind::Up,
4 => PointerKind::Hover,
5 => PointerKind::Leave,
_ => PointerKind::Cancel,
};
let button = match button {
1 => MouseButton::Middle,
2 => MouseButton::Right,
3 => MouseButton::Back,
4 => MouseButton::Forward,
_ => MouseButton::Left,
};
state(s).app.pointer_button(kind, button, x, y, time_ms);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_context_menu(s: *mut WebState, x: f32, y: f32, source: i32, time_ms: f64) -> i32 {
let source = match source {
1 => ContextMenuSource::Touch,
2 => ContextMenuSource::Keyboard,
_ => ContextMenuSource::Mouse,
};
state(s).app.context_menu(x, y, source, time_ms) as i32
}
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "C" fn dui_key(
s: *mut WebState,
kind: i32,
code: *const u8,
code_len: usize,
text: *const u8,
text_len: usize,
modifiers: u32,
repeat: i32,
) -> i32 {
let read = |ptr: *const u8, len: usize| match ptr.is_null() {
true => "",
false => std::str::from_utf8(unsafe { std::slice::from_raw_parts(ptr, len) }).unwrap_or(""),
};
let kind = match kind {
0 => KeyKind::Down,
1 => KeyKind::Up,
_ => KeyKind::Char,
};
let key = if kind == KeyKind::Char { "" } else { key_name(read(code, code_len)) };
state(s).app.key(kind, key, read(text, text_len), Modifiers::from_bits(modifiers), repeat != 0) as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_safe_insets(s: *mut WebState, left: f32, top: f32, right: f32, bottom: f32) {
state(s).app.safe_insets(Thickness { left, top, right, bottom });
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_history(s: *mut WebState, on: i32) {
state(s).host.history_on = on != 0;
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_location(s: *mut WebState, hash: *const u8, hash_len: usize, popped: i32) {
let hash = if hash.is_null() { "" } else { std::str::from_utf8(unsafe { std::slice::from_raw_parts(hash, hash_len) }).unwrap_or("") };
state(s).app.location(hash, (popped >= 0).then_some(popped as u32));
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_visibility(s: *mut WebState, visible: i32) {
state(s).app.visibility(visible != 0);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_blur(s: *mut WebState) {
state(s).app.blur();
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_focus_out(s: *mut WebState) {
state(s).app.focus_out();
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_a11y_activate(s: *mut WebState, id: u32) {
state(s).app.accessibility_activate(id);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_a11y_focus(s: *mut WebState, id: u32, focused: i32) {
state(s).app.accessibility_focus(id, focused != 0);
}
fn url(hash: Option<String>) -> String {
match hash {
Some(hash) if hash.is_empty() => "#".to_owned(),
Some(hash) => hash,
None => String::new(),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_poll_output(s: *mut WebState) -> *const c_char {
let s = state(s);
let host = &mut s.host;
let message = if let Some(cursor) = host.cursor.take() {
let n = match cursor {
Cursor::Default => 0,
Cursor::Pointer => 1,
Cursor::Text => 2,
};
Some(format!("c {n}"))
} else if let Some(area) = host.text_input.take() {
let node = host.text_input_node.map_or(String::new(), |n| n.to_string());
Some(area.map_or_else(|| "t".to_owned(), |r| format!("t {} {} {} {} {node}", r.left, r.top, r.width(), r.height())))
} else if let Some(text) = host.clipboard.take() {
Some(format!("p {text}"))
} else if let Some(url) = (!host.urls.is_empty()).then(|| host.urls.remove(0)) {
Some(format!("u {url}"))
} else if let Some(op) = (!host.history.is_empty()).then(|| host.history.remove(0)) {
Some(match op {
HistoryOp::Push { depth, hash } => format!("h push {depth} {}", url(hash)),
HistoryOp::Replace { depth, hash } => format!("h replace {depth} {}", url(hash)),
HistoryOp::Back => "h back".to_owned(),
})
} else if std::mem::take(&mut host.paste) {
Some("v".to_owned())
} else if let Some(nodes) = host.accessibility.take() {
use std::fmt::Write;
let clean = |t: &str| t.replace(['\t', '\n', '\r', '\u{1e}', '\u{1f}'], " ");
let mut out = String::from("a");
for n in &nodes {
if n.role == crate::ui::Aria::SCROLL_VIEW {
continue;
}
let parent = n.parent.and_then(|p| nodes.iter().find(|m| m.id == p));
if parent.is_some_and(|p| p.can_interact && crate::ui::said_by_child(std::slice::from_ref(n), p)) {
continue;
}
let named = n.can_interact || !crate::ui::said_by_child(&nodes, n);
let pressed = n.is_pressed.map_or(-1, |p| p as i32);
let (r, label, hint) = (n.rect, if named { clean(&n.label) } else { String::new() }, clean(&n.hint));
let interactive = if n.can_interact { "1" } else if crate::ui::is_control_role(n.role) { "-" } else { "0" };
let (id, role, live) = (n.id, n.role, n.live);
let (left, top, width, height) = (r.left, r.top, r.width(), r.height());
let group = n.group.map_or(String::new(), |(g, i)| format!("{g}:{i}"));
let value = n.value.as_ref().map_or(String::new(), |v| format!("{}\u{1f}{}\u{1f}{}\u{1f}{}", v.now, v.min, v.max, clean(&v.text)));
let _ = write!(out, "\n{id}\t{role}\t{label}\t{hint}\t{left}\t{top}\t{width}\t{height}\t{interactive}\t{pressed}\t{live}\t{group}\t{value}\t");
for (i, l) in n.text_lines.iter().enumerate() {
let (text, family) = (clean(&l.text), clean(&l.font_family));
let separator = if i > 0 { "\u{1e}" } else { "" };
let _ = write!(out, "{separator}{text}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{family}\u{1f}{}\u{1f}{}", l.left, l.top, l.width, l.height, l.font_weight, l.font_size);
}
}
Some(out)
} else if let Some(id) = host.keyboard_moved.take() {
Some(format!("f {id}"))
} else {
None
};
s.output = message.map(|m| {
CString::new(m).unwrap_or_else(|e| {
let at = e.nul_position();
CString::new(&e.into_vec()[..at]).unwrap_or_default()
})
});
s.output.as_ref().map_or(std::ptr::null(), |m| m.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_wheel(s: *mut WebState, x: f32, y: f32, delta: f32, time_ms: f64) -> i32 {
state(s).app.wheel(x, y, delta, false, time_ms) as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_wheel_axis(s: *mut WebState, x: f32, y: f32, delta: f32, horizontal: i32, time_ms: f64) -> i32 {
state(s).app.wheel(x, y, delta, horizontal != 0, time_ms) as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_poll_request(s: *mut WebState) -> *const c_char {
let s = state(s);
s.request = s.host.requests.pop().and_then(|(id, url)| CString::new(format!("{id} {url}")).ok());
s.request.as_ref().map_or(std::ptr::null(), |r| r.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_poll_image(s: *mut WebState) -> *const c_char {
let s = state(s);
let next = (!s.host.images.is_empty()).then(|| s.host.images.remove(0));
s.request = next.and_then(|r| {
CString::new(format!("{} {} {} {} {}", r.id, r.width, r.height, r.frames as u8, r.source)).ok()
});
s.request.as_ref().map_or(std::ptr::null(), |r| r.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_alloc_pixels(s: *mut WebState, len: usize) -> *mut u8 {
let data = unsafe { skia::Data::new_uninitialized(len) };
let ptr = data.as_bytes().as_ptr() as *mut u8;
state(s).pixels.push(data);
ptr
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_image(
s: *mut WebState,
id: u32,
ptr: *const u8,
width: i32,
height: i32,
source_width: i32,
source_height: i32,
opaque: i32,
) {
let s = state(s);
let decoded = s.pixels.iter().position(|data| data.as_bytes().as_ptr() == ptr).and_then(|at| {
let alpha = if opaque != 0 { skia::AlphaType::Opaque } else { skia::AlphaType::Premul };
let info = skia::ImageInfo::new((width, height), skia::ColorType::RGBA8888, alpha, None);
let image = skia::images::raster_from_data(&info, s.pixels.swap_remove(at), info.min_row_bytes())?;
Some(Decoded { image, source_size: skia::ISize::new(source_width, source_height), frames: None })
});
s.app.image(id, decoded);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_image_frames(
s: *mut WebState,
id: u32,
ptr: *const u8,
width: i32,
height: i32,
count: usize,
durations: *mut u8,
) {
let s = state(s);
let ms: Vec<u32> = if durations.is_null() {
Vec::new()
} else {
let bytes = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(durations, count * 4)) };
bytes.chunks_exact(4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()
};
let decoded = s.pixels.iter().position(|data| data.as_bytes().as_ptr() == ptr).and_then(|at| {
let info = skia::ImageInfo::new((width, height), skia::ColorType::RGBA8888, skia::AlphaType::Premul, None);
let (row_bytes, all) = (info.min_row_bytes(), s.pixels.swap_remove(at));
let frame_bytes = info.compute_byte_size(row_bytes);
let images: Vec<skia::Image> = (0..count)
.filter_map(|i| skia::Data::new_subset(&all, i * frame_bytes, frame_bytes).into())
.filter_map(|data| skia::images::raster_from_data(&info, data, row_bytes))
.collect();
let image = images.first()?.clone();
let size = skia::ISize::new(width, height);
let frames = (images.len() > 1).then(|| Arc::new(Frames { images, durations: ms, size }));
Some(Decoded { image, source_size: size, frames })
});
s.app.image(id, decoded);
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_alloc(len: usize) -> *mut u8 {
Box::into_raw(vec![0u8; len].into_boxed_slice()) as *mut u8
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_free(ptr: *mut u8, len: usize) {
if !ptr.is_null() {
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) });
}
}
#[unsafe(no_mangle)]
pub extern "C" fn dui_asset(s: *mut WebState, id: u32, ptr: *mut u8, len: usize) {
let bytes = if ptr.is_null() { Vec::new() } else { unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) }.into_vec() };
state(s).app.asset(id, bytes);
}