cgs 0.1.0

a ui library for macos
use std::cell::RefCell;
use std::time::Duration;

use objc2::rc::{Retained, Weak};
use objc2::{DefinedClass, MainThreadOnly, Message, define_class, msg_send};
use objc2_app_kit::*;
use objc2_core_graphics::CGContext;
use objc2_foundation::{NSString, NSTimer};
use objc2_quartz_core::CALayer;

use crate::bridge::callback;
use crate::{CGRect, Label, NativeView, SecondaryLabel, Ui, VStack, View};

type DrawCallback = Box<dyn FnMut(&CGContext, CGRect)>;
type KeyCallback = Box<dyn FnMut(&NSEvent) -> bool>;
struct CanvasState {
    draw: RefCell<DrawCallback>,
    key: RefCell<Option<KeyCallback>>,
}
define_class!(
    #[unsafe(super(NSView))]
    #[thread_kind = MainThreadOnly]
    #[name = "CgUiCanvas"]
    #[ivars = CanvasState]
    struct CanvasView;
    impl CanvasView {
        #[unsafe(method(drawRect:))]
        fn draw(&self,_dirty:CGRect){let _keep_alive=self.retain();if let Some(context)=NSGraphicsContext::currentContext(){let context=context.CGContext();let cg=context.as_ref();CGContext::save_g_state(Some(cg));if let Ok(mut draw)=self.ivars().draw.try_borrow_mut(){callback(||draw(cg,self.bounds()));}CGContext::restore_g_state(Some(cg));}}
        #[unsafe(method(viewDidChangeEffectiveAppearance))]
        fn appearance_changed(&self){unsafe{let _:()=msg_send![super(self),viewDidChangeEffectiveAppearance];}self.setNeedsDisplay(true);}
        #[unsafe(method(acceptsFirstResponder))]
        fn accepts_first_responder(&self)->bool{self.ivars().key.borrow().is_some()}
        #[unsafe(method(mouseDown:))]
        fn mouse_down(&self,event:&NSEvent){
            if self.ivars().key.borrow().is_some(){if let Some(window)=self.window(){window.makeFirstResponder(Some(self));}}
            else{unsafe{let _:()=msg_send![super(self),mouseDown:event];}}
        }
        #[unsafe(method(focusRingMaskBounds))]
        fn focus_ring_bounds(&self)->CGRect{self.bounds()}
        #[unsafe(method(drawFocusRingMask))]
        fn focus_ring_mask(&self){if let Some(context)=NSGraphicsContext::currentContext(){let context=context.CGContext();CGContext::fill_rect(Some(context.as_ref()),self.bounds());}}
        #[unsafe(method(keyDown:))]
        fn key_down(&self,event:&NSEvent){let _keep_alive=self.retain();let mut handled=false;if let Ok(mut key)=self.ivars().key.try_borrow_mut(){if let Some(key)=key.as_mut(){callback(||handled=key(event));}}if !handled{unsafe{let _:()=msg_send![super(self),keyDown:event];}}}
    }
);
pub struct Canvas(Retained<CanvasView>);
impl Canvas {
    pub fn new(ui: &Ui, draw: impl FnMut(&CGContext, CGRect) + 'static) -> Self {
        let this = CanvasView::alloc(ui.mtm()).set_ivars(CanvasState {
            draw: RefCell::new(Box::new(draw)),
            key: RefCell::new(None),
        });
        Self(unsafe { msg_send![super(this), init] })
    }

    pub fn on_key_down(self, f: impl FnMut(&NSEvent) -> bool + 'static) -> Self {
        *self.0.ivars().key.borrow_mut() = Some(Box::new(f));
        self.0.setFocusRingType(NSFocusRingType::Exterior);
        self.0.setAccessibilityElement(true);
        self.0.setAccessibilityRole(Some(unsafe { NSAccessibilityGroupRole }));
        self
    }

    pub fn redraw(&self) { self.0.setNeedsDisplay(true); }

    pub fn ns_view(&self) -> &NSView { &self.0 }
}
impl NativeView for Canvas {
    fn ns_view(&self) -> &NSView { &self.0 }
}

pub struct LayerHost(View);
impl LayerHost {
    pub fn new(ui: &Ui, layer: &CALayer) -> Self {
        let view = View::new(ui);
        view.ns_view().setWantsLayer(true);
        view.ns_view().setLayer(Some(layer));
        Self(view)
    }

    pub fn ns_view(&self) -> &NSView { self.0.ns_view() }
}
impl NativeView for LayerHost {
    fn ns_view(&self) -> &NSView { self.0.ns_view() }
}

pub struct VisualEffect {
    native: Retained<NSVisualEffectView>,
    content: Box<dyn NativeView>,
}
impl VisualEffect {
    pub fn new(ui: &Ui, material: NSVisualEffectMaterial, content: impl NativeView) -> Self {
        let native = NSVisualEffectView::new(ui.mtm());
        native.setMaterial(material);
        native.setState(NSVisualEffectState::FollowsWindowActiveState);
        native.addSubview(content.ns_view());
        crate::view::pin(&native, content.ns_view(), crate::Insets {
            top: 0.0,
            left: 0.0,
            bottom: 0.0,
            right: 0.0,
        });
        Self {
            native,
            content: Box::new(content),
        }
    }

    pub fn sidebar(ui: &Ui, content: impl NativeView) -> Self {
        Self::new(ui, NSVisualEffectMaterial::Sidebar, content)
    }

    pub fn ns_visual_effect_view(&self) -> &NSVisualEffectView { &self.native }

    pub fn content_view(&self) -> &NSView { self.content.ns_view() }
}
impl NativeView for VisualEffect {
    fn ns_view(&self) -> &NSView { &self.native }
}
pub struct BackgroundView(View);
impl BackgroundView {
    pub fn new(ui: &Ui, content: impl NativeView) -> Self {
        Self(View::new(ui).content(content, crate::Insets {
            top: 0.0,
            left: 0.0,
            bottom: 0.0,
            right: 0.0,
        }))
    }

    pub fn ns_view(&self) -> &NSView { self.0.ns_view() }
}
impl NativeView for BackgroundView {
    fn ns_view(&self) -> &NSView { self.0.ns_view() }
}

pub struct ProgressIndicator(Retained<NSProgressIndicator>);
impl ProgressIndicator {
    pub fn new(ui: &Ui) -> Self {
        let native = NSProgressIndicator::new(ui.mtm());
        native.setStyle(NSProgressIndicatorStyle::Spinning);
        native.setIndeterminate(true);
        Self(native)
    }

    pub fn start(&self) {
        unsafe {
            self.0.startAnimation(None);
        }
    }

    pub fn stop(&self) {
        unsafe {
            self.0.stopAnimation(None);
        }
    }

    pub fn fraction(&self, value: f64) {
        self.0.setIndeterminate(false);
        self.0.setMinValue(0.0);
        self.0.setMaxValue(1.0);
        self.0.setDoubleValue(value);
    }

    pub fn ns_progress_indicator(&self) -> &NSProgressIndicator { &self.0 }
}
impl NativeView for ProgressIndicator {
    fn ns_view(&self) -> &NSView { &self.0 }
}
pub struct EmptyState(VStack);
impl EmptyState {
    pub fn new(ui: &Ui, title: &str, description: &str) -> Self {
        Self(
            VStack::new(ui)
                .push(crate::SectionTitle::new(ui, title))
                .push(SecondaryLabel::new(ui, description)),
        )
    }

    pub fn action(self, button: crate::Button) -> Self { Self(self.0.push(button)) }
}
impl NativeView for EmptyState {
    fn ns_view(&self) -> &NSView { self.0.ns_view() }
}
pub struct LoadingView(VStack);
impl LoadingView {
    pub fn new(ui: &Ui, title: &str) -> Self {
        let progress = ProgressIndicator::new(ui);
        progress.start();
        Self(VStack::new(ui).push(progress).push(Label::new(ui, title)))
    }
}
impl NativeView for LoadingView {
    fn ns_view(&self) -> &NSView { self.0.ns_view() }
}
pub type StatusLabel = SecondaryLabel;
pub struct TransientStatus {
    label: StatusLabel,
    timer: RefCell<Option<Retained<NSTimer>>>,
    target: Retained<crate::bridge::ActionTarget>,
}
impl TransientStatus {
    pub fn new(ui: &Ui) -> Self {
        let label = StatusLabel::new(ui, "");
        label.set_hidden(true);
        let target = crate::bridge::ActionTarget::new(ui);
        let weak = Weak::new(label.ns_view());
        target.set(move |_| {
            if let Some(view) = weak.load() {
                view.setHidden(true);
            }
        });
        Self {
            label,
            timer: RefCell::new(None),
            target,
        }
    }

    pub fn show(&self, text: &str, duration: Duration) {
        self.clear();
        self.label.set_text(text);
        self.label.set_hidden(false);
        let timer = unsafe {
            NSTimer::scheduledTimerWithTimeInterval_target_selector_userInfo_repeats(
                duration.as_secs_f64(),
                &self.target,
                objc2::sel!(invoke:),
                None,
                false,
            )
        };
        *self.timer.borrow_mut() = Some(timer);
    }

    pub fn clear(&self) {
        if let Some(timer) = self.timer.borrow_mut().take() {
            timer.invalidate();
        }
        self.label.set_hidden(true);
    }

    pub fn ns_text_field(&self) -> &NSTextField { self.label.ns_text_field() }
}
impl NativeView for TransientStatus {
    fn ns_view(&self) -> &NSView { self.label.ns_view() }
}
impl Drop for TransientStatus {
    fn drop(&mut self) { self.clear(); }
}
pub struct Clipboard;
impl Clipboard {
    pub fn set_text(ui: &Ui, text: &str) -> bool {
        let _ = ui;
        let pasteboard = NSPasteboard::generalPasteboard();
        pasteboard.clearContents();
        pasteboard.setString_forType(&NSString::from_str(text), unsafe { NSPasteboardTypeString })
    }

    pub fn text(ui: &Ui) -> Option<String> {
        let _ = ui;
        NSPasteboard::generalPasteboard()
            .stringForType(unsafe { NSPasteboardTypeString })
            .map(|s| s.to_string())
    }
}