use super::*;
struct CApp {
user: *mut c_void,
view: ViewFn,
on_event: Option<EventFn>,
teardown: Option<TeardownFn>,
}
static ON_TEARDOWN: std::sync::Mutex<Option<TeardownFn>> = std::sync::Mutex::new(None);
impl kui_native::App for CApp {
fn view(&mut self, ui: &mut kui_native::Ui<'_>) {
let mut shim = KuiCtx::borrowing_in(ui);
(self.view)(self.user, &mut shim);
}
fn teardown(&mut self) {
if let Some(cb) = self.teardown {
cb(self.user);
}
}
fn on_event(&mut self, ev: kui_native::UiEvent) {
let Some(cb) = self.on_event else { return };
let payload = KuiValue(ev.payload);
let out = KuiEvent {
origin: ev.origin.0,
key: ev.key.0,
payload: &payload,
window: ev.window.0,
slot: ev.slot.map_or(0, |k| k.0),
..Default::default()
};
cb(self.user, &out);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_on_teardown(teardown: TeardownFn) {
guard((), || {
*ON_TEARDOWN.lock().unwrap_or_else(|e| e.into_inner()) = Some(teardown);
})
}
#[derive(Debug, PartialEq)]
struct IconAsk {
pixels: Option<(Vec<u8>, u32, u32)>,
resource: Option<u16>,
}
static ICON: std::sync::Mutex<Option<IconAsk>> = std::sync::Mutex::new(None);
unsafe fn icon_ask_of(
rgba: *const u8,
width: u32,
height: u32,
resource: u32,
) -> Result<Option<IconAsk>, String> {
let pixels = match (rgba.is_null(), width, height) {
(true, 0, 0) => None,
(false, w, h) if w > 0 && h > 0 => {
let len = (w as usize)
.checked_mul(h as usize)
.and_then(|n| n.checked_mul(4))
.filter(|&n| n <= isize::MAX as usize)
.ok_or_else(|| format!("a {w}x{h} icon is too large to address"))?;
Some((
unsafe { std::slice::from_raw_parts(rgba, len) }.to_vec(),
w,
h,
))
}
(true, w, h) => return Err(format!("a {w}x{h} icon with NULL pixels")),
(false, w, h) => return Err(format!("an icon is at least one pixel, not {w}x{h}")),
};
let resource = match resource {
0 => None,
r => Some(
u16::try_from(r).map_err(|_| format!("an icon resource id is 1 to 65535, not {r}"))?,
),
};
Ok((pixels.is_some() || resource.is_some()).then_some(IconAsk { pixels, resource }))
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_set_icon(rgba: *const u8, width: u32, height: u32, resource: u32) -> bool {
guard(false, || {
match unsafe { icon_ask_of(rgba, width, height, resource) } {
Ok(ask) => {
*ICON.lock().unwrap_or_else(|e| e.into_inner()) = ask;
true
}
Err(why) => {
eprintln!("kui: kui_set_icon: {why}");
false
}
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_run(
title: KuiStr,
view: ViewFn,
on_event: EventFn,
user: *mut c_void,
) -> bool {
kui_run_with(
std::ptr::null_mut(),
title,
std::ptr::null(),
view,
on_event,
user,
)
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_run_with(
ptr: *mut KuiCtx,
title: KuiStr,
config: *const KuiRunConfig,
view: ViewFn,
on_event: EventFn,
user: *mut c_void,
) -> bool {
guard(false, || {
let options = match run_options_of(unsafe { config.as_ref() }) {
Ok(o) => o,
Err(why) => {
eprintln!("kui: kui_run_with: {why}");
return false;
}
};
let mut launcher = launcher_for(&kstr(title), options);
if let Some(icon) = ICON.lock().unwrap_or_else(|e| e.into_inner()).take() {
if let Some((rgba, w, h)) = icon.pixels {
launcher = match launcher.try_icon(rgba, w, h) {
Ok(l) => l,
Err(why) => {
eprintln!("kui: kui_run_with: {why}");
return false;
}
};
}
if let Some(id) = icon.resource {
launcher = launcher.icon_resource(id);
}
}
if let Some(c) = unsafe { ctx(ptr) } {
launcher = launcher
.with_extensions(std::mem::take(&mut c.extensions))
.system(c.core().env.system);
if let Some(core) = c.take_core() {
launcher = launcher.core(*core);
}
}
let app = CApp {
user,
view,
on_event: Some(on_event),
teardown: ON_TEARDOWN.lock().unwrap_or_else(|e| e.into_inner()).take(),
};
launcher.run(app).is_ok()
})
}
fn launcher_for(title: &str, options: RunOptions) -> kui_native::Launcher {
let mut l = kui_native::app(title);
if let Some((w, h)) = options.size {
l = l.size(w, h);
}
if let Some((w, h)) = options.min_size {
l = l.min_size(w, h);
}
if let Some((w, h)) = options.max_size {
l = l.max_size(w, h);
}
l = l.chrome(match options.chrome {
KUI_CHROME_CUSTOM => kui_native::Chrome::Custom,
KUI_CHROME_BORDERLESS => kui_native::Chrome::Borderless,
_ => kui_native::Chrome::Native,
});
l = l.text_aa(match options.text_aa {
KUI_TEXT_AA_GRAYSCALE => kui_native::TextAa::Grayscale,
KUI_TEXT_AA_SUBPIXEL => kui_native::TextAa::Subpixel,
_ => kui_native::TextAa::Auto,
});
if let Some(on) = options.diagnostics {
l = l.diagnostics(on);
}
if let Some(frames) = options.frame_latency {
l = l.frame_latency(frames);
}
l.backdrop(options.backdrop)
.titlebar(match options.titlebar {
KUI_TITLEBAR_MEDIUM => kui_native::Titlebar::Medium,
KUI_TITLEBAR_TALL => kui_native::Titlebar::Tall,
_ => kui_native::Titlebar::Standard,
})
}
fn hand_out(rgba: &[u8], delays: &[f64]) -> (*mut u8, *const f64) {
let px_words = rgba.len().div_ceil(8);
let words = 2 + px_words + delays.len();
let mut block = vec![0u64; words].into_boxed_slice();
block[0] = words as u64;
let base = Box::into_raw(block).cast::<u64>();
unsafe {
let px = base.add(2).cast::<u8>();
std::ptr::copy_nonoverlapping(rgba.as_ptr(), px, rgba.len());
let d = base.add(2 + px_words).cast::<f64>();
std::ptr::copy_nonoverlapping(delays.as_ptr(), d, delays.len());
(px, d)
}
}
unsafe fn bytes_of<'a>(bytes: *const u8, len: usize) -> &'a [u8] {
if bytes.is_null() || len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(bytes, len) }
}
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_decode_image(
bytes: *const u8,
len: usize,
width: *mut u32,
height: *mut u32,
) -> *mut u8 {
guard(std::ptr::null_mut(), || {
let put = |w: u32, h: u32| unsafe {
if !width.is_null() {
*width = w;
}
if !height.is_null() {
*height = h;
}
};
put(0, 0);
match kui_native::decode_image(unsafe { bytes_of(bytes, len) }) {
Ok(p) => {
put(p.width, p.height);
hand_out(&p.rgba, &[]).0
}
Err(why) => {
eprintln!("kui: kui_decode_image: {why}");
std::ptr::null_mut()
}
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_decode_animation(
bytes: *const u8,
len: usize,
width: *mut u32,
height: *mut u32,
count: *mut u32,
loops: *mut u32,
delays: *mut *const f64,
) -> *mut u8 {
guard(std::ptr::null_mut(), || {
let put = |w: u32, h: u32, n: u32, l: u32, d: *const f64| unsafe {
for (p, v) in [(width, w), (height, h), (count, n), (loops, l)] {
if !p.is_null() {
*p = v;
}
}
if !delays.is_null() {
*delays = d;
}
};
put(0, 0, 0, 0, std::ptr::null());
match kui_native::decode_animation(unsafe { bytes_of(bytes, len) }) {
Ok(a) => {
let rgba: Vec<u8> = a
.frames
.iter()
.flat_map(|f| f.rgba.iter().copied())
.collect();
let ds: Vec<f64> = a.frames.iter().map(|f| f.delay).collect();
let (px, d) = hand_out(&rgba, &ds);
put(a.width, a.height, ds.len() as u32, a.loops.unwrap_or(0), d);
px
}
Err(why) => {
eprintln!("kui: kui_decode_animation: {why}");
std::ptr::null_mut()
}
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_animation_at(
delays: *const f64,
count: u32,
loops: u32,
elapsed: f64,
next: *mut f64,
) -> u32 {
guard(0, || {
let ds: &[f64] = if delays.is_null() || count == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(delays, count as usize) }
};
let a = kui_native::Animation {
width: 0,
height: 0,
frames: ds
.iter()
.map(|&delay| kui_native::AnimationFrame {
rgba: Vec::new(),
delay,
})
.collect(),
loops: (loops != 0).then_some(loops),
};
let s = a.at(elapsed);
if !next.is_null() {
unsafe { *next = s.next };
}
s.index as u32
})
}
#[unsafe(no_mangle)]
pub extern "C" fn kui_pixels_free(pixels: *mut u8) {
guard((), || {
if pixels.is_null() {
return;
}
unsafe {
let base = pixels.cast::<u64>().sub(2);
let words = *base as usize;
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
base, words,
)));
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_icon_is_pixels_of_its_size_and_a_16_bit_resource() {
let px = [7u8; 2 * 3 * 4];
let ask = |rgba: *const u8, w, h, r| unsafe { icon_ask_of(rgba, w, h, r) };
assert_eq!(ask(std::ptr::null(), 0, 0, 0), Ok(None), "all zero is none");
assert_eq!(
ask(px.as_ptr(), 2, 3, 1),
Ok(Some(IconAsk {
pixels: Some((px.to_vec(), 2, 3)),
resource: Some(1)
}))
);
assert_eq!(
ask(std::ptr::null(), 0, 0, 65535),
Ok(Some(IconAsk {
pixels: None,
resource: Some(65535)
}))
);
assert!(
ask(std::ptr::null(), 2, 3, 0)
.unwrap_err()
.contains("NULL pixels")
);
assert!(
ask(px.as_ptr(), 0, 3, 0)
.unwrap_err()
.contains("at least one pixel")
);
assert!(
ask(px.as_ptr(), 2, 3, 65536)
.unwrap_err()
.contains("1 to 65535")
);
}
}