cocoanut 0.2.3

A minimal, declarative macOS GUI framework for Rust
use crate::view::View;

/// RGBA color representation
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
    pub r: f64,
    pub g: f64,
    pub b: f64,
    pub a: f64,
}

impl Color {
    pub const RED: Color = Color {
        r: 1.0,
        g: 0.0,
        b: 0.0,
        a: 1.0,
    };
    pub const GREEN: Color = Color {
        r: 0.0,
        g: 1.0,
        b: 0.0,
        a: 1.0,
    };
    pub const BLUE: Color = Color {
        r: 0.0,
        g: 0.0,
        b: 1.0,
        a: 1.0,
    };
    pub const YELLOW: Color = Color {
        r: 1.0,
        g: 1.0,
        b: 0.0,
        a: 1.0,
    };
    pub const ORANGE: Color = Color {
        r: 1.0,
        g: 0.5,
        b: 0.0,
        a: 1.0,
    };
    pub const PURPLE: Color = Color {
        r: 0.5,
        g: 0.0,
        b: 0.5,
        a: 1.0,
    };
    pub const PINK: Color = Color {
        r: 1.0,
        g: 0.0,
        b: 0.5,
        a: 1.0,
    };
    pub const WHITE: Color = Color {
        r: 1.0,
        g: 1.0,
        b: 1.0,
        a: 1.0,
    };
    pub const BLACK: Color = Color {
        r: 0.0,
        g: 0.0,
        b: 0.0,
        a: 1.0,
    };
    pub const CLEAR: Color = Color {
        r: 0.0,
        g: 0.0,
        b: 0.0,
        a: 0.0,
    };
    pub const GRAY: Color = Color {
        r: 0.5,
        g: 0.5,
        b: 0.5,
        a: 1.0,
    };

    pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self {
        Self { r, g, b, a }
    }

    pub fn rgb(r: f64, g: f64, b: f64) -> Self {
        Self { r, g, b, a: 1.0 }
    }

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

    pub fn to_system_name(&self) -> Option<&'static str> {
        if *self == Self::RED {
            return Some("systemRed");
        }
        if *self == Self::GREEN {
            return Some("systemGreen");
        }
        if *self == Self::BLUE {
            return Some("systemBlue");
        }
        if *self == Self::YELLOW {
            return Some("systemYellow");
        }
        if *self == Self::ORANGE {
            return Some("systemOrange");
        }
        if *self == Self::PURPLE {
            return Some("systemPurple");
        }
        if *self == Self::PINK {
            return Some("systemPink");
        }
        if *self == Self::GRAY {
            return Some("systemGray");
        }
        if *self == Self::WHITE {
            return Some("whiteColor");
        }
        if *self == Self::BLACK {
            return Some("blackColor");
        }
        if *self == Self::CLEAR {
            return Some("clearColor");
        }
        None
    }
}

impl Default for Color {
    fn default() -> Self {
        Self::BLACK
    }
}

/// CSS-like styling DSL for views
///
/// # Examples
///
/// ```
/// use cocoanut::prelude::*;
///
/// let btn = View::button("Click")
///     .styled(&style().width(100.0).padding(10.0).background("blue"));
/// ```
#[derive(Debug, Clone, Default)]
#[must_use = "Style must be applied to a View with .apply() or .styled()"]
pub struct Style {
    pub width: Option<f64>,
    pub height: Option<f64>,
    pub padding: Option<f64>,
    pub margin: Option<f64>,
    pub background: Option<String>,
    pub foreground: Option<String>,
    pub font_size: Option<f64>,
    pub font_weight: Option<FontWeight>,
    pub corner_radius: Option<f64>,
    pub border_width: Option<f64>,
    pub border_color: Option<String>,
    pub opacity: Option<f64>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FontWeight {
    Normal,
    Bold,
    Light,
}

impl Style {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn width(mut self, w: f64) -> Self {
        self.width = Some(w);
        self
    }

    pub fn height(mut self, h: f64) -> Self {
        self.height = Some(h);
        self
    }

    pub fn size(mut self, w: f64, h: f64) -> Self {
        self.width = Some(w);
        self.height = Some(h);
        self
    }

    pub fn padding(mut self, p: f64) -> Self {
        self.padding = Some(p);
        self
    }

    pub fn margin(mut self, m: f64) -> Self {
        self.margin = Some(m);
        self
    }

    pub fn background(mut self, color: impl Into<String>) -> Self {
        self.background = Some(color.into());
        self
    }

    pub fn foreground(mut self, color: impl Into<String>) -> Self {
        self.foreground = Some(color.into());
        self
    }

    pub fn font_size(mut self, size: f64) -> Self {
        self.font_size = Some(size);
        self
    }

    pub fn bold(mut self) -> Self {
        self.font_weight = Some(FontWeight::Bold);
        self
    }

    pub fn corner_radius(mut self, r: f64) -> Self {
        self.corner_radius = Some(r);
        self
    }

    pub fn border(mut self, width: f64, color: impl Into<String>) -> Self {
        self.border_width = Some(width);
        self.border_color = Some(color.into());
        self
    }

    pub fn opacity(mut self, o: f64) -> Self {
        self.opacity = Some(o);
        self
    }

    pub fn apply(&self, mut view: View) -> View {
        if let Some(w) = self.width {
            view.style.width = Some(w);
        }
        if let Some(h) = self.height {
            view.style.height = Some(h);
        }
        if let Some(p) = self.padding {
            view.style.padding = Some(p);
        }
        if let Some(m) = self.margin {
            view.style.margin = Some(m);
        }
        if let Some(ref c) = self.background {
            view.style.background = Some(c.clone());
        }
        if let Some(ref c) = self.foreground {
            view.style.foreground = Some(c.clone());
        }
        if let Some(s) = self.font_size {
            view.style.font_size = Some(s);
        }
        if let Some(FontWeight::Bold) = self.font_weight {
            view.style.font_bold = true;
        }
        if let Some(r) = self.corner_radius {
            view.style.corner_radius = Some(r);
        }
        if let Some(w) = self.border_width {
            view.style.border_width = Some(w);
        }
        if let Some(ref c) = self.border_color {
            view.style.border_color = Some(c.clone());
        }
        if let Some(o) = self.opacity {
            view.style.opacity = Some(o);
        }
        view
    }
}

pub fn style() -> Style {
    Style::new()
}

pub fn color(name: &str) -> String {
    match name.to_lowercase().as_str() {
        "red" => "systemRed".to_string(),
        "green" => "systemGreen".to_string(),
        "blue" => "systemBlue".to_string(),
        "yellow" => "systemYellow".to_string(),
        "orange" => "systemOrange".to_string(),
        "purple" => "systemPurple".to_string(),
        "pink" => "systemPink".to_string(),
        "gray" | "grey" => "systemGray".to_string(),
        "white" => "whiteColor".to_string(),
        "black" => "blackColor".to_string(),
        "clear" => "clearColor".to_string(),
        _ => name.to_string(),
    }
}

pub fn font(name: &str) -> String {
    match name.to_lowercase().as_str() {
        "title" => "title".to_string(),
        "headline" => "headline".to_string(),
        "body" => "body".to_string(),
        "caption" => "caption".to_string(),
        "footnote" => "footnote".to_string(),
        _ => name.to_string(),
    }
}

pub trait Styled {
    fn styled(self, style: &Style) -> Self;
}

impl Styled for View {
    fn styled(self, style: &Style) -> Self {
        style.apply(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn color_new_rgb_with_alpha() {
        let c = Color::rgb(0.2, 0.4, 0.6).with_alpha(0.5);
        assert!((c.r - 0.2).abs() < 1e-9);
        assert!((c.a - 0.5).abs() < 1e-9);
    }

    #[test]
    fn color_to_system_name_known_colors() {
        assert_eq!(Color::RED.to_system_name(), Some("systemRed"));
        assert_eq!(Color::GRAY.to_system_name(), Some("systemGray"));
        assert_eq!(Color::CLEAR.to_system_name(), Some("clearColor"));
        assert_eq!(Color::new(0.1, 0.2, 0.3, 1.0).to_system_name(), None);
    }

    #[test]
    fn style_builder_apply_to_view() {
        let s = style()
            .size(120.0, 48.0)
            .padding(8.0)
            .margin(4.0)
            .background("systemBlue")
            .foreground("whiteColor")
            .font_size(15.0)
            .bold()
            .corner_radius(6.0)
            .border(1.0, "blackColor")
            .opacity(0.9);

        let v = View::button("OK").styled(&s);
        assert_eq!(v.style.width, Some(120.0));
        assert_eq!(v.style.height, Some(48.0));
        assert_eq!(v.style.padding, Some(8.0));
        assert_eq!(v.style.margin, Some(4.0));
        assert_eq!(v.style.background.as_deref(), Some("systemBlue"));
        assert_eq!(v.style.foreground.as_deref(), Some("whiteColor"));
        assert_eq!(v.style.font_size, Some(15.0));
        assert!(v.style.font_bold);
        assert_eq!(v.style.corner_radius, Some(6.0));
        assert_eq!(v.style.border_width, Some(1.0));
        assert_eq!(v.style.border_color.as_deref(), Some("blackColor"));
        assert_eq!(v.style.opacity, Some(0.9));
    }

    #[test]
    fn color_and_font_helpers() {
        assert_eq!(color("ReD"), "systemRed");
        assert_eq!(color("grey"), "systemGray");
        assert_eq!(color("custom"), "custom");
        assert_eq!(font("TITLE"), "title");
        assert_eq!(font("unknown"), "unknown");
    }
}