use std::collections::VecDeque;
use std::ffi::{CStr, c_char};
use std::panic::{AssertUnwindSafe, catch_unwind};
use denise_ui::Ui;
pub mod input;
pub mod keys;
pub mod paint;
pub mod tree;
pub mod types;
pub use input::*;
pub use paint::*;
pub use tree::*;
pub use types::*;
pub const DENISE_ABI_VERSION: u32 = 1;
pub const DENISE_OK: i32 = 0;
pub const DENISE_ERR_NULL: i32 = -1;
pub const DENISE_ERR_INVALID: i32 = -2;
pub const DENISE_ERR_NO_NODE: i32 = -3;
pub const DENISE_ERR_BUFFER_TOO_SMALL: i32 = -4;
pub const DENISE_ERR_WRONG_WIDGET: i32 = -5;
pub const DENISE_ERR_PANIC: i32 = -6;
static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
#[unsafe(no_mangle)]
pub extern "C" fn denise_version() -> *const c_char {
VERSION.as_ptr().cast()
}
#[unsafe(no_mangle)]
pub extern "C" fn denise_abi_version() -> u32 {
DENISE_ABI_VERSION
}
#[unsafe(no_mangle)]
pub extern "C" fn denise_status_message(status: i32) -> *const c_char {
let text: &CStr = match status {
DENISE_OK => c"ok",
DENISE_ERR_NULL => c"a required pointer was NULL",
DENISE_ERR_INVALID => c"an argument was out of range, or a string was not UTF-8",
DENISE_ERR_NO_NODE => c"the node id does not name a live node",
DENISE_ERR_BUFFER_TOO_SMALL => c"the buffer supplied is too small",
DENISE_ERR_WRONG_WIDGET => c"the node is not a widget of that kind",
DENISE_ERR_PANIC => c"denise panicked; the call did nothing",
_ => c"unknown status",
};
text.as_ptr()
}
pub struct DeniseUi {
ui: Ui<u32>,
pending: VecDeque<u32>,
}
impl DeniseUi {
fn collect(&mut self) {
self.pending
.extend(self.ui.drain_messages().filter(|&m| m != 0));
}
}
pub(crate) fn guard<T>(fallback: T, body: impl FnOnce() -> T) -> T {
match catch_unwind(AssertUnwindSafe(body)) {
Ok(value) => value,
Err(_) => fallback,
}
}
pub(crate) unsafe fn handle<'a>(ui: *mut DeniseUi) -> Option<&'a mut DeniseUi> {
unsafe { ui.as_mut() }
}
pub(crate) unsafe fn utf8<'a>(text: *const c_char) -> Option<&'a str> {
if text.is_null() {
return None;
}
unsafe { CStr::from_ptr(text) }.to_str().ok()
}
#[unsafe(no_mangle)]
pub extern "C" fn denise_ui_new(width: u32, height: u32, theme: u32) -> *mut DeniseUi {
guard(std::ptr::null_mut(), || {
let Some(theme) = types::theme(theme) else {
return std::ptr::null_mut();
};
if width == 0 || height == 0 {
return std::ptr::null_mut();
}
Box::into_raw(Box::new(DeniseUi {
ui: Ui::new(denise::Size::new(width, height), theme),
pending: VecDeque::new(),
}))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn denise_ui_new_scaled(
width: u32,
height: u32,
theme: u32,
scale_x100: u32,
) -> *mut DeniseUi {
guard(std::ptr::null_mut(), || {
let Some(theme) = types::theme(theme) else {
return std::ptr::null_mut();
};
if width == 0 || height == 0 || scale_x100 == 0 {
return std::ptr::null_mut();
}
let theme = theme.scaled(scale_x100 as f32 / 100.0);
Box::into_raw(Box::new(DeniseUi {
ui: Ui::new(denise::Size::new(width, height), theme),
pending: VecDeque::new(),
}))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn denise_ui_free(ui: *mut DeniseUi) {
if ui.is_null() {
return;
}
guard((), || {
drop(unsafe { Box::from_raw(ui) });
});
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn denise_ui_set_theme(ui: *mut DeniseUi, theme: u32) -> i32 {
guard(DENISE_ERR_PANIC, || {
let Some(handle) = (unsafe { handle(ui) }) else {
return DENISE_ERR_NULL;
};
let Some(theme) = types::theme(theme) else {
return DENISE_ERR_INVALID;
};
handle.ui.set_theme(theme);
DENISE_OK
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn denise_ui_size(
ui: *mut DeniseUi,
width: *mut u32,
height: *mut u32,
) -> i32 {
guard(DENISE_ERR_PANIC, || {
let Some(handle) = (unsafe { handle(ui) }) else {
return DENISE_ERR_NULL;
};
let size = handle.ui.size();
if !width.is_null() {
unsafe { width.write(size.width) };
}
if !height.is_null() {
unsafe { height.write(size.height) };
}
DENISE_OK
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn denise_ui_poll_message(ui: *mut DeniseUi, out: *mut u32) -> bool {
guard(false, || {
let Some(handle) = (unsafe { handle(ui) }) else {
return false;
};
handle.collect();
let Some(message) = handle.pending.pop_front() else {
return false;
};
if !out.is_null() {
unsafe { out.write(message) };
}
true
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_version_string_is_nul_terminated_and_matches_the_crate() {
assert!(VERSION.ends_with('\0'));
let text = unsafe { CStr::from_ptr(denise_version()) };
assert_eq!(text.to_str().unwrap(), env!("CARGO_PKG_VERSION"));
}
#[test]
fn every_status_has_its_own_message() {
let codes = [
DENISE_OK,
DENISE_ERR_NULL,
DENISE_ERR_INVALID,
DENISE_ERR_NO_NODE,
DENISE_ERR_BUFFER_TOO_SMALL,
DENISE_ERR_WRONG_WIDGET,
DENISE_ERR_PANIC,
];
let mut seen: Vec<&str> = Vec::new();
for code in codes {
let text = unsafe { CStr::from_ptr(denise_status_message(code)) }
.to_str()
.unwrap();
assert!(
!seen.contains(&text),
"{code} shares a message with another"
);
seen.push(text);
}
let unknown = unsafe { CStr::from_ptr(denise_status_message(-999)) };
assert_eq!(unknown.to_str().unwrap(), "unknown status");
}
#[test]
fn a_null_handle_is_refused_rather_than_dereferenced() {
unsafe {
denise_ui_free(std::ptr::null_mut());
assert_eq!(
denise_ui_set_theme(std::ptr::null_mut(), 0),
DENISE_ERR_NULL
);
assert_eq!(
denise_ui_size(
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut()
),
DENISE_ERR_NULL
);
assert!(!denise_ui_poll_message(
std::ptr::null_mut(),
std::ptr::null_mut()
));
}
}
#[test]
fn a_bad_size_or_theme_produces_no_handle() {
assert!(denise_ui_new(0, 480, DENISE_THEME_DARK).is_null());
assert!(denise_ui_new(800, 0, DENISE_THEME_DARK).is_null());
assert!(denise_ui_new(800, 480, 99).is_null());
let ui = denise_ui_new(800, 480, DENISE_THEME_DARK);
assert!(!ui.is_null());
unsafe { denise_ui_free(ui) };
}
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct Readme;