cocoanut 0.2.3

A minimal, declarative macOS GUI framework for Rust
use crate::error::{CocoanutError, Result};
use crate::view::View;

/// Dark mode preference for the application
///
/// # Examples
///
/// ```
/// use cocoanut::prelude::*;
///
/// let app = app("My App")
///     .appearance(Appearance::Dark)
///     .build();
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Appearance {
    /// Light mode (Aqua appearance)
    Light,
    /// Dark mode (Dark Aqua appearance)
    Dark,
    /// System default (follows macOS appearance settings)
    System,
}

/// The main application structure
///
/// Use the [`app()`] function and builder pattern to create an app.
#[derive(Debug)]
#[allow(dead_code)] // window chrome hooks not wired through yet
#[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>,
}

/// Builder for creating an App
///
/// # Examples
///
/// ```no_run
/// use cocoanut::prelude::*;
///
/// app("My App")
///     .size(800.0, 600.0)
///     .centered(true)
///     .dark()
///     .build()
///     .root(View::text("Hello"))
///     .run()?;
/// # Ok::<(), cocoanut::CocoanutError>(())
/// ```
#[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 {
    /// Set the root view tree
    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()
    }

    /// Run the application event loop
    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 {
            // 1. Init NSApplication
            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);
            }
            // Set activation policy to regular (shows in dock, brings to front)
            let _: () = msg_send![ns_app, setActivationPolicy: 0_i64];

            // 2. Create window
            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());
            }

            // 3. Set title
            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];
            }

            // 4. Apply appearance
            apply_appearance(ns_window, self.appearance);

            // 5. Initialize event system
            crate::event::init();
            #[cfg(not(test))]
            {
                let _ = crate::event::register_action_class();
            }

            // 6. Render view tree into content view (with padding)
            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)?;

            // 7. Show window
            let _: () = msg_send![ns_window, makeKeyAndOrderFront: ns_app];
            let _: () = msg_send![ns_app, activateIgnoringOtherApps: true];
            let _: () = msg_send![ns_window, orderFrontRegardless];

            // 8. Run event loop
            let _: () = msg_send![ns_app, run];
        }
        Ok(())
    }
}

/// Convenience function: create an app builder
pub fn app(title: impl Into<String>) -> AppBuilder {
    AppBuilder::new(title)
}

/// Apply appearance (dark/light/system) to a window
#[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];
        }
    }
}

/// Set appearance at runtime (can be called from callbacks)
#[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);
    }
}