use crate::error::{CocoanutError, Result};
use crate::view::View;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Appearance {
Light,
Dark,
System,
}
#[derive(Debug)]
#[allow(dead_code)] #[must_use = "App must be run with .run() to start the application"]
pub struct App {
title: String,
width: f64,
height: f64,
centered: bool,
appearance: Appearance,
resizable: bool,
closable: bool,
minimizable: bool,
on_close_callback: Option<usize>,
on_resize_callback: Option<usize>,
root: Option<View>,
}
#[derive(Debug)]
#[must_use = "AppBuilder must be built with .build() to create an App"]
pub struct AppBuilder {
title: String,
width: f64,
height: f64,
centered: bool,
appearance: Appearance,
resizable: bool,
closable: bool,
minimizable: bool,
on_close_callback: Option<usize>,
on_resize_callback: Option<usize>,
}
impl AppBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
width: 800.0,
height: 600.0,
centered: true,
appearance: Appearance::System,
resizable: true,
closable: true,
minimizable: true,
on_close_callback: None,
on_resize_callback: None,
}
}
pub fn size(mut self, w: f64, h: f64) -> Self {
self.width = w;
self.height = h;
self
}
pub fn centered(mut self, c: bool) -> Self {
self.centered = c;
self
}
pub fn appearance(mut self, a: Appearance) -> Self {
self.appearance = a;
self
}
pub fn dark(self) -> Self {
self.appearance(Appearance::Dark)
}
pub fn light(self) -> Self {
self.appearance(Appearance::Light)
}
pub fn resizable(mut self, r: bool) -> Self {
self.resizable = r;
self
}
pub fn closable(mut self, c: bool) -> Self {
self.closable = c;
self
}
pub fn minimizable(mut self, m: bool) -> Self {
self.minimizable = m;
self
}
pub fn on_close(mut self, callback_id: usize) -> Self {
self.on_close_callback = Some(callback_id);
self
}
pub fn on_close_fn(mut self, f: impl Fn() + Send + 'static) -> Self {
let id = crate::event::register_with_auto_id(f);
self.on_close_callback = Some(id);
self
}
pub fn on_resize(mut self, callback_id: usize) -> Self {
self.on_resize_callback = Some(callback_id);
self
}
pub fn on_resize_fn(mut self, f: impl Fn(f64, f64) + Send + 'static) -> Self {
let id = crate::event::register_size_with_auto_id(f);
self.on_resize_callback = Some(id);
self
}
pub fn build(self) -> App {
App {
title: self.title,
width: self.width,
height: self.height,
centered: self.centered,
appearance: self.appearance,
root: None,
resizable: self.resizable,
closable: self.closable,
minimizable: self.minimizable,
on_close_callback: self.on_close_callback,
on_resize_callback: self.on_resize_callback,
}
}
}
impl App {
pub fn root(mut self, view: View) -> Self {
self.root = Some(view);
self
}
pub fn title(&self) -> &str {
&self.title
}
pub fn width(&self) -> f64 {
self.width
}
pub fn height(&self) -> f64 {
self.height
}
pub fn root_view(&self) -> Option<&View> {
self.root.as_ref()
}
pub fn run(mut self) -> Result<()> {
let root = self.root.take().ok_or(CocoanutError::NoRootView)?;
#[cfg(test)]
{
println!(
"✓ App '{}' ({}x{})",
self.title, self.width as i32, self.height as i32
);
if self.centered {
println!("✓ Window centered");
}
crate::renderer::render(
&root,
std::ptr::null_mut(),
(0.0, 0.0, self.width, self.height),
)?;
println!("✓ Event loop running (test stub)");
Ok(())
}
#[cfg(not(test))]
{
self.run_appkit(&root)
}
}
#[cfg(not(test))]
fn run_appkit(&self, root: &View) -> Result<()> {
use cocoa::foundation::{NSPoint, NSRect, NSSize};
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
unsafe {
let ns_app: *mut Object = msg_send![
Class::get("NSApplication").ok_or("NSApplication not found")?,
sharedApplication
];
if ns_app.is_null() {
return Err(CocoanutError::NotInitialized);
}
let _: () = msg_send![ns_app, setActivationPolicy: 0_i64];
let frame = NSRect {
origin: NSPoint { x: 100.0, y: 100.0 },
size: NSSize {
width: self.width,
height: self.height,
},
};
let ns_window_cls = Class::get("NSWindow").ok_or("NSWindow not found")?;
let ns_window_alloc: *mut Object = msg_send![ns_window_cls, alloc];
let ns_window: *mut Object = msg_send![ns_window_alloc, initWithContentRect:frame styleMask:15_u64 backing:2_u64 defer:false];
if ns_window.is_null() {
return Err("Failed to create window".into());
}
let title_cstr = std::ffi::CString::new(self.title.as_str())?;
let title_ns: *mut Object =
msg_send![objc::class!(NSString), stringWithUTF8String: title_cstr.as_ptr()];
let _: () = msg_send![ns_window, setTitle: title_ns];
if self.centered {
let _: () = msg_send![ns_window, center];
}
apply_appearance(ns_window, self.appearance);
crate::event::init();
#[cfg(not(test))]
{
let _ = crate::event::register_action_class();
}
let content_view: *mut Object = msg_send![ns_window, contentView];
let raw_bounds: NSRect = msg_send![content_view, bounds];
let pad = 20.0;
let content_bounds = NSRect {
origin: NSPoint {
x: raw_bounds.origin.x + pad,
y: raw_bounds.origin.y + pad,
},
size: NSSize {
width: raw_bounds.size.width - pad * 2.0,
height: raw_bounds.size.height - pad * 2.0,
},
};
crate::renderer::render(root, content_view, content_bounds)?;
let _: () = msg_send![ns_window, makeKeyAndOrderFront: ns_app];
let _: () = msg_send![ns_app, activateIgnoringOtherApps: true];
let _: () = msg_send![ns_window, orderFrontRegardless];
let _: () = msg_send![ns_app, run];
}
Ok(())
}
}
pub fn app(title: impl Into<String>) -> AppBuilder {
AppBuilder::new(title)
}
#[cfg(not(test))]
fn apply_appearance(window: *mut objc::runtime::Object, appearance: Appearance) {
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
if appearance == Appearance::System {
return;
}
let name = match appearance {
Appearance::Dark => "NSAppearanceNameDarkAqua",
Appearance::Light => "NSAppearanceNameAqua",
Appearance::System => return,
};
unsafe {
let name_cstr = std::ffi::CString::new(name).unwrap();
let name_ns: *mut Object =
msg_send![objc::class!(NSString), stringWithUTF8String: name_cstr.as_ptr()];
let cls = Class::get("NSAppearance").unwrap();
let app_obj: *mut Object = msg_send![cls, appearanceNamed: name_ns];
if !app_obj.is_null() {
let _: () = msg_send![window, setAppearance: app_obj];
}
}
}
#[cfg(not(test))]
pub fn set_appearance(appearance: Appearance) {
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
unsafe {
let ns_app: *mut Object =
msg_send![Class::get("NSApplication").unwrap(), sharedApplication];
let window: *mut Object = msg_send![ns_app, keyWindow];
if !window.is_null() {
apply_appearance(window, appearance);
}
}
}
#[cfg(test)]
pub fn set_appearance(_appearance: Appearance) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_builder() {
let a = app("Test").size(640.0, 480.0).centered(false).build();
assert_eq!(a.title(), "Test");
assert_eq!(a.width(), 640.0);
assert_eq!(a.height(), 480.0);
assert!(a.root_view().is_none());
}
#[test]
fn test_app_run_without_root_errors() {
let a = app("Test").build();
let result = a.run();
assert!(result.is_err());
}
#[test]
fn test_app_with_root() {
let a = app("Test").build().root(View::text("hello"));
assert!(a.root_view().is_some());
}
#[test]
fn appearance_variants() {
use Appearance::*;
let _ = format!("{:?}", Dark);
set_appearance(System);
set_appearance(Light);
}
}