cgs 0.1.0

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

use objc2::rc::Retained;
use objc2_app_kit::*;
use objc2_foundation::NSArray;

use crate::view::prepare;
use crate::{Insets, Metrics, NativeView, Ui, View};

pub struct Stack {
    native: Retained<NSStackView>,
    children: RefCell<Vec<Box<dyn NativeView>>>,
}
impl Stack {
    pub fn new(ui: &Ui, orientation: NSUserInterfaceLayoutOrientation) -> Self {
        let native = NSStackView::new(ui.mtm());
        prepare(&native);
        native.setOrientation(orientation);
        native.setSpacing(Metrics::CONTROL_SPACING);
        native.setAlignment(if orientation == NSUserInterfaceLayoutOrientation::Vertical {
            NSLayoutAttribute::Leading
        } else {
            NSLayoutAttribute::CenterY
        });
        Self {
            native,
            children: RefCell::new(Vec::new()),
        }
    }

    pub fn ns_stack_view(&self) -> &NSStackView { &self.native }

    pub fn spacing(self, value: f64) -> Self {
        self.native.setSpacing(value);
        self
    }

    pub fn alignment(self, value: NSLayoutAttribute) -> Self {
        self.native.setAlignment(value);
        self
    }

    pub fn distribution(self, value: NSStackViewDistribution) -> Self {
        self.native.setDistribution(value);
        self
    }

    pub fn insets(self, value: Insets) -> Self {
        self.native.setEdgeInsets(value);
        self
    }

    pub fn push(self, view: impl NativeView) -> Self {
        self.add(view);
        self
    }

    pub fn add(&self, view: impl NativeView) {
        prepare(view.ns_view());
        self.native.addArrangedSubview(view.ns_view());
        if self.native.orientation() == NSUserInterfaceLayoutOrientation::Vertical {
            let insets = self.native.edgeInsets();
            view.ns_view()
                .widthAnchor()
                .constraintEqualToAnchor_constant(
                    &self.native.widthAnchor(),
                    -insets.left - insets.right,
                )
                .setActive(true);
        }
        self.children.borrow_mut().push(Box::new(view));
    }

    pub fn clear(&self) {
        for child in self.children.borrow_mut().drain(..) {
            self.native.removeArrangedSubview(child.ns_view());
            child.ns_view().removeFromSuperview();
        }
    }

    pub fn spacer(self, ui: &Ui) -> Self { self.push(Spacer::new(ui)) }
}
impl NativeView for Stack {
    fn ns_view(&self) -> &NSView { &self.native }
}
macro_rules! stack {
    ($name:ident, $axis:ident) => {
        pub struct $name(Stack);
        impl $name {
            pub fn new(ui: &Ui) -> Self {
                Self(Stack::new(ui, NSUserInterfaceLayoutOrientation::$axis))
            }

            pub fn spacing(self, value: f64) -> Self { Self(self.0.spacing(value)) }

            pub fn alignment(self, value: NSLayoutAttribute) -> Self {
                Self(self.0.alignment(value))
            }

            pub fn distribution(self, value: NSStackViewDistribution) -> Self {
                Self(self.0.distribution(value))
            }

            pub fn insets(self, value: Insets) -> Self { Self(self.0.insets(value)) }

            pub fn push(self, view: impl NativeView) -> Self { Self(self.0.push(view)) }

            pub fn add(&self, view: impl NativeView) { self.0.add(view); }

            pub fn clear(&self) { self.0.clear(); }

            pub fn spacer(self, ui: &Ui) -> Self { Self(self.0.spacer(ui)) }

            pub fn ns_stack_view(&self) -> &NSStackView { self.0.ns_stack_view() }
        }
        impl NativeView for $name {
            fn ns_view(&self) -> &NSView { self.0.ns_view() }
        }
    };
}
stack!(VStack, Vertical);
stack!(HStack, Horizontal);

pub struct Spacer(View);
impl Spacer {
    pub fn new(ui: &Ui) -> Self {
        let view = View::new(ui);
        prepare(view.ns_view());
        for axis in [
            NSLayoutConstraintOrientation::Horizontal,
            NSLayoutConstraintOrientation::Vertical,
        ] {
            view.ns_view().setContentHuggingPriority_forOrientation(1.0, axis);
            view.ns_view().setContentCompressionResistancePriority_forOrientation(1.0, axis);
        }
        Self(view)
    }
}
impl NativeView for Spacer {
    fn ns_view(&self) -> &NSView { self.0.ns_view() }
}

pub struct Grid {
    native: Retained<NSGridView>,
    children: Vec<Box<dyn NativeView>>,
}
impl Grid {
    pub fn new(ui: &Ui) -> Self {
        let native = NSGridView::new(ui.mtm());
        prepare(&native);
        native.setXPlacement(NSGridCellPlacement::Leading);
        native.setYPlacement(NSGridCellPlacement::Center);
        Self { native, children: Vec::new() }
    }

    pub fn row(mut self, cells: Vec<Box<dyn NativeView>>) -> Self {
        let views: Vec<&NSView> = cells.iter().map(|v| v.ns_view()).collect();
        self.native.addRowWithViews(&NSArray::from_slice(&views));
        self.children.extend(cells);
        self
    }

    pub fn spacing(self, rows: f64, columns: f64) -> Self {
        self.native.setRowSpacing(rows);
        self.native.setColumnSpacing(columns);
        self
    }

    pub fn ns_grid_view(&self) -> &NSGridView { &self.native }
}
impl NativeView for Grid {
    fn ns_view(&self) -> &NSView { &self.native }
}

pub struct Form {
    ui: Ui,
    grid: Grid,
}
impl Form {
    pub fn new(ui: &Ui) -> Self {
        Self {
            ui: *ui,
            grid: Grid::new(ui).spacing(Metrics::ROW_SPACING, Metrics::CONTROL_SPACING),
        }
    }

    pub fn row(mut self, title: &str, control: impl NativeView) -> Self {
        self.grid = self.grid.row(vec![
            Box::new(crate::Label::new(&self.ui, title)),
            Box::new(control),
        ]);
        self
    }

    pub fn help(mut self, text: &str) -> Self {
        self.grid = self.grid.row(vec![
            Box::new(crate::Label::new(&self.ui, "")),
            Box::new(crate::SecondaryLabel::new(&self.ui, text)),
        ]);
        self
    }

    pub fn ns_grid_view(&self) -> &NSGridView { self.grid.ns_grid_view() }
}
impl NativeView for Form {
    fn ns_view(&self) -> &NSView { self.grid.ns_view() }
}

pub struct Divider(Retained<NSBox>);
impl Divider {
    pub fn new(ui: &Ui) -> Self {
        let native = NSBox::new(ui.mtm());
        native.setBoxType(NSBoxType::Separator);
        Self(native)
    }

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

/// A quiet native group surface; semantic fill follows the window appearance.
pub struct GroupBox {
    native: Retained<NSBox>,
    _content: Box<dyn NativeView>,
}
impl GroupBox {
    pub fn new(ui: &Ui, content: impl NativeView) -> Self {
        Self::with_insets(ui, content, Insets {
            top: 10.0,
            left: 12.0,
            bottom: 10.0,
            right: 12.0,
        })
    }

    pub fn with_insets(ui: &Ui, content: impl NativeView, insets: Insets) -> Self {
        let native = NSBox::new(ui.mtm());
        native.setBoxType(NSBoxType::Custom);
        native.setTitlePosition(NSTitlePosition::NoTitle);
        native.setBorderWidth(0.0);
        native.setCornerRadius(8.0);
        native.setFillColor(&NSColor::quaternarySystemFillColor());
        native.setContentViewMargins(crate::CGSize::new(0.0, 0.0));
        let host = native.contentView().unwrap();
        host.addSubview(content.ns_view());
        crate::view::pin(&host, content.ns_view(), insets);
        Self {
            native,
            _content: Box::new(content),
        }
    }
}
impl NativeView for GroupBox {
    fn ns_view(&self) -> &NSView { &self.native }
}

pub struct ScrollView {
    native: Retained<NSScrollView>,
    content: Box<dyn NativeView>,
}
impl ScrollView {
    pub fn new(ui: &Ui, content: impl NativeView) -> Self {
        let native = NSScrollView::new(ui.mtm());
        native.setHasVerticalScroller(true);
        native.setAutohidesScrollers(true);
        native.setDrawsBackground(false);
        native.setDocumentView(Some(content.ns_view()));
        Self {
            native,
            content: Box::new(content),
        }
    }

    pub fn ns_scroll_view(&self) -> &NSScrollView { &self.native }

    pub fn content_view(&self) -> &NSView { self.content.ns_view() }

    /// Pin the document to the viewport width while allowing vertical scrolling.
    pub fn fit_width(&self) {
        let clip = self.native.contentView();
        let child = self.content.ns_view();
        prepare(child);
        child
            .leadingAnchor()
            .constraintEqualToAnchor(&clip.leadingAnchor())
            .setActive(true);
        child.topAnchor().constraintEqualToAnchor(&clip.topAnchor()).setActive(true);
        child.widthAnchor().constraintEqualToAnchor(&clip.widthAnchor()).setActive(true);
    }
}
impl NativeView for ScrollView {
    fn ns_view(&self) -> &NSView { &self.native }
}

pub struct Disclosure {
    stack: VStack,
    button: crate::Button,
    body: Box<dyn NativeView>,
}
impl Disclosure {
    pub fn new(ui: &Ui, title: &str, content: impl NativeView) -> Self {
        use objc2::rc::Weak;
        let body = Box::new(content);
        body.set_hidden(true);
        let weak = Weak::new(body.ns_view());
        let button = crate::Button::new(ui, title).disclosure().on_click(move || {
            if let Some(view) = weak.load() {
                view.setHidden(!view.isHidden());
            }
        });
        button.set_title("");
        button.accessibility_label(title);
        let heading = HStack::new(ui).push(crate::Label::new(ui, title));
        heading.ns_stack_view().insertArrangedSubview_atIndex(button.ns_view(), 0);
        let stack = VStack::new(ui);
        stack.add(heading);
        stack.ns_stack_view().addArrangedSubview(body.ns_view());
        Self { stack, button, body }
    }

    pub fn set_expanded(&self, value: bool) {
        self.body.set_hidden(!value);
        self.button.ns_button().setState(if value {
            NSControlStateValueOn
        } else {
            NSControlStateValueOff
        });
    }

    pub fn ns_button(&self) -> &NSButton { self.button.ns_button() }
}
impl NativeView for Disclosure {
    fn ns_view(&self) -> &NSView { self.stack.ns_view() }
}