cocoanut 0.2.3

A minimal, declarative macOS GUI framework for Rust
//! Auto Layout support for native Cocoa NSLayoutConstraint
//!
//! This module provides a type-safe wrapper around NSLayoutConstraint
//! for building constraint-based layouts instead of frame-based layouts.

use crate::error::Result;

use objc::runtime::Object;

#[cfg(not(test))]
use objc::{msg_send, sel, sel_impl};

/// Layout constraint attributes (maps to NSLayoutAttribute)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LayoutAttribute {
    Left = 1,
    Right = 2,
    Top = 3,
    Bottom = 4,
    Leading = 5,
    Trailing = 6,
    Width = 7,
    Height = 8,
    CenterX = 9,
    CenterY = 10,
    Baseline = 11,
    FirstBaseline = 12,
}

/// Layout constraint relation (maps to NSLayoutRelation)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutRelation {
    LessThanOrEqual = -1,
    Equal = 0,
    GreaterThanOrEqual = 1,
}

/// Layout priority (maps to NSLayoutPriority)
#[derive(Debug, Clone, Copy)]
pub struct LayoutPriority(pub f32);

impl LayoutPriority {
    pub const REQUIRED: LayoutPriority = LayoutPriority(1000.0);
    pub const HIGH: LayoutPriority = LayoutPriority(750.0);
    pub const MEDIUM: LayoutPriority = LayoutPriority(500.0);
    pub const LOW: LayoutPriority = LayoutPriority(250.0);
    pub const FITTING_SIZE_COMPRESSION: LayoutPriority = LayoutPriority(750.0);
}

/// A constraint descriptor that can be applied to views
#[derive(Debug, Clone)]
#[must_use = "ConstraintDescriptor must be added to a View with .constraint()"]
pub struct ConstraintDescriptor {
    pub first_attribute: LayoutAttribute,
    pub relation: LayoutRelation,
    pub second_attribute: Option<LayoutAttribute>,
    pub multiplier: f64,
    pub constant: f64,
    pub priority: LayoutPriority,
}

impl Default for ConstraintDescriptor {
    fn default() -> Self {
        Self {
            first_attribute: LayoutAttribute::Width,
            relation: LayoutRelation::Equal,
            second_attribute: None,
            multiplier: 1.0,
            constant: 0.0,
            priority: LayoutPriority::REQUIRED,
        }
    }
}

impl ConstraintDescriptor {
    pub fn new(attr: LayoutAttribute) -> Self {
        Self {
            first_attribute: attr,
            relation: LayoutRelation::Equal,
            second_attribute: None,
            multiplier: 1.0,
            constant: 0.0,
            priority: LayoutPriority::REQUIRED,
        }
    }

    pub fn equal_to(mut self, attr: LayoutAttribute) -> Self {
        self.second_attribute = Some(attr);
        self.relation = LayoutRelation::Equal;
        self
    }

    pub fn constant(mut self, c: f64) -> Self {
        self.constant = c;
        self
    }

    pub fn priority(mut self, p: LayoutPriority) -> Self {
        self.priority = p;
        self
    }
}

/// Helper to create common constraints
pub mod constraints {
    use super::*;

    /// Pin view to all edges of superview
    pub fn fill_superview() -> Vec<ConstraintDescriptor> {
        vec![
            ConstraintDescriptor::new(LayoutAttribute::Top).equal_to(LayoutAttribute::Top),
            ConstraintDescriptor::new(LayoutAttribute::Bottom).equal_to(LayoutAttribute::Bottom),
            ConstraintDescriptor::new(LayoutAttribute::Leading).equal_to(LayoutAttribute::Leading),
            ConstraintDescriptor::new(LayoutAttribute::Trailing)
                .equal_to(LayoutAttribute::Trailing),
        ]
    }

    /// Center view in superview
    pub fn center_in_superview() -> Vec<ConstraintDescriptor> {
        vec![
            ConstraintDescriptor::new(LayoutAttribute::CenterX).equal_to(LayoutAttribute::CenterX),
            ConstraintDescriptor::new(LayoutAttribute::CenterY).equal_to(LayoutAttribute::CenterY),
        ]
    }

    /// Set fixed width
    pub fn width(w: f64) -> ConstraintDescriptor {
        ConstraintDescriptor::new(LayoutAttribute::Width).constant(w)
    }

    /// Set fixed height
    pub fn height(h: f64) -> ConstraintDescriptor {
        ConstraintDescriptor::new(LayoutAttribute::Height).constant(h)
    }

    /// Set aspect ratio (width/height)
    pub fn aspect_ratio(ratio: f64) -> ConstraintDescriptor {
        let mut c = ConstraintDescriptor::new(LayoutAttribute::Width);
        c.second_attribute = Some(LayoutAttribute::Height);
        c.multiplier = ratio;
        c
    }
}

/// Apply Auto Layout constraints to a view
///
/// # Safety
/// The `view` and `superview` pointers must be valid NSView objects.
#[cfg(not(test))]
pub unsafe fn apply_constraints(
    view: *mut Object,
    superview: *mut Object,
    descriptors: &[ConstraintDescriptor],
) -> Result<()> {
    // Disable autoresizing mask translation
    let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints: false];

    let constraint_class = objc::class!(NSLayoutConstraint);

    for desc in descriptors {
        let constraint: *mut Object = if let Some(second_attr) = desc.second_attribute {
            // Constraint relative to superview
            msg_send![
                constraint_class,
                constraintWithItem: view
                attribute: desc.first_attribute as i64
                relatedBy: desc.relation as i64
                toItem: superview
                attribute: second_attr as i64
                multiplier: desc.multiplier
                constant: desc.constant
            ]
        } else {
            // Constraint on self (width/height)
            msg_send![
                constraint_class,
                constraintWithItem: view
                attribute: desc.first_attribute as i64
                relatedBy: desc.relation as i64
                toItem: std::ptr::null_mut::<Object>()
                attribute: 0_i64
                multiplier: 1.0
                constant: desc.constant
            ]
        };

        let _: () = msg_send![constraint, setPriority: desc.priority.0];
        let _: () = msg_send![superview, addConstraint: constraint];
    }
    Ok(())
}

/// Enable/disable Auto Layout for a view
///
/// # Safety
/// The `view` pointer must be a valid NSView object.
#[cfg(not(test))]
pub unsafe fn set_uses_auto_layout(view: *mut Object, uses: bool) {
    let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints: !uses];
}

/// Get intrinsic content size of a view
///
/// # Safety
/// The `view` pointer must be a valid NSView object.
#[cfg(not(test))]
pub unsafe fn intrinsic_content_size(view: *mut Object) -> (f64, f64) {
    let size: cocoa::foundation::NSSize = msg_send![view, intrinsicContentSize];
    (size.width, size.height)
}

#[cfg(test)]
pub unsafe fn apply_constraints(
    _view: *mut Object,
    _superview: *mut Object,
    _descriptors: &[ConstraintDescriptor],
) -> Result<()> {
    Ok(())
}

#[cfg(test)]
pub unsafe fn set_uses_auto_layout(_view: *mut Object, _uses: bool) {}

#[cfg(test)]
pub unsafe fn intrinsic_content_size(_view: *mut Object) -> (f64, f64) {
    (0.0, 0.0)
}

#[cfg(test)]
mod tests {
    use super::constraints;
    use super::{ConstraintDescriptor, LayoutAttribute, LayoutPriority, LayoutRelation};

    #[test]
    fn constraint_descriptor_new_and_chain() {
        let d = ConstraintDescriptor::new(LayoutAttribute::Width)
            .equal_to(LayoutAttribute::Height)
            .constant(12.5)
            .priority(LayoutPriority::HIGH);
        assert_eq!(d.first_attribute, LayoutAttribute::Width);
        assert_eq!(d.second_attribute, Some(LayoutAttribute::Height));
        assert_eq!(d.constant, 12.5);
        assert_eq!(d.relation, LayoutRelation::Equal);
        assert_eq!(d.priority.0, 750.0);
    }

    #[test]
    fn constraint_default() {
        let d = ConstraintDescriptor::default();
        assert_eq!(d.first_attribute, LayoutAttribute::Width);
        assert_eq!(d.multiplier, 1.0);
    }

    #[test]
    fn fill_superview_four_constraints() {
        let v = constraints::fill_superview();
        assert_eq!(v.len(), 4);
    }

    #[test]
    fn center_in_superview_two() {
        let v = constraints::center_in_superview();
        assert_eq!(v.len(), 2);
    }

    #[test]
    fn width_height_aspect_helpers() {
        let w = constraints::width(200.0);
        assert_eq!(w.first_attribute, LayoutAttribute::Width);
        assert_eq!(w.constant, 200.0);

        let h = constraints::height(44.0);
        assert_eq!(h.first_attribute, LayoutAttribute::Height);
        assert_eq!(h.constant, 44.0);

        let a = constraints::aspect_ratio(16.0 / 9.0);
        assert_eq!(a.first_attribute, LayoutAttribute::Width);
        assert_eq!(a.second_attribute, Some(LayoutAttribute::Height));
        assert!((a.multiplier - 16.0 / 9.0).abs() < f64::EPSILON);
    }

    #[test]
    fn layout_attribute_relation_discriminant() {
        assert_eq!(LayoutAttribute::Left as i32, 1);
        assert_eq!(LayoutRelation::Equal as i32, 0);
    }

    #[test]
    fn apply_constraints_mock_ok() {
        let descs = constraints::width(100.0);
        let r = unsafe { super::apply_constraints(std::ptr::null_mut(), std::ptr::null_mut(), &[descs]) };
        assert!(r.is_ok());
    }
}