operad 8.0.1

A cross-platform GUI library for Rust.
Documentation
use super::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ThemePreference {
    System,
    Light,
    Dark,
}

impl ThemePreference {
    pub const ALL: [Self; 3] = [Self::System, Self::Light, Self::Dark];

    pub const fn as_str(self) -> &'static str {
        match self {
            Self::System => "system",
            Self::Light => "light",
            Self::Dark => "dark",
        }
    }

    pub const fn label(self) -> &'static str {
        match self {
            Self::System => "System",
            Self::Light => "Light",
            Self::Dark => "Dark",
        }
    }

    pub const fn is_dark(self) -> bool {
        matches!(self, Self::Dark)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThemePreferenceLabels {
    pub system: String,
    pub light: String,
    pub dark: String,
    pub switch: String,
}

impl ThemePreferenceLabels {
    pub fn label(&self, preference: ThemePreference) -> &str {
        match preference {
            ThemePreference::System => &self.system,
            ThemePreference::Light => &self.light,
            ThemePreference::Dark => &self.dark,
        }
    }
}

impl Default for ThemePreferenceLabels {
    fn default() -> Self {
        Self {
            system: "System".to_owned(),
            light: "Light".to_owned(),
            dark: "Dark".to_owned(),
            switch: "Dark theme".to_owned(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ThemePreferenceButtonsOptions {
    pub layout: LayoutStyle,
    pub button_options: ButtonOptions,
    pub labels: ThemePreferenceLabels,
    pub include_system: bool,
    pub enabled: bool,
    pub action_prefix: Option<String>,
    pub accessibility_label: Option<String>,
}

impl ThemePreferenceButtonsOptions {
    pub fn with_layout(mut self, layout: impl Into<LayoutStyle>) -> Self {
        self.layout = layout.into();
        self
    }

    pub fn with_button_options(mut self, options: ButtonOptions) -> Self {
        self.button_options = options;
        self
    }

    pub fn with_labels(mut self, labels: ThemePreferenceLabels) -> Self {
        self.labels = labels;
        self
    }

    pub const fn include_system(mut self, include_system: bool) -> Self {
        self.include_system = include_system;
        self
    }

    pub const fn enabled(mut self, enabled: bool) -> Self {
        self.enabled = enabled;
        self
    }

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

    pub fn without_actions(mut self) -> Self {
        self.action_prefix = None;
        self
    }

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

    fn action_for(&self, preference: ThemePreference) -> Option<WidgetActionBinding> {
        self.action_prefix
            .as_ref()
            .map(|prefix| WidgetActionBinding::action(format!("{prefix}.{}", preference.as_str())))
    }
}

impl Default for ThemePreferenceButtonsOptions {
    fn default() -> Self {
        Self {
            layout: LayoutStyle::from_taffy_style(Style {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: Some(AlignItems::Center),
                gap: TaffySize {
                    width: taffy::prelude::LengthPercentage::length(4.0),
                    height: taffy::prelude::LengthPercentage::length(4.0),
                },
                ..Default::default()
            }),
            button_options: ButtonOptions {
                layout: LayoutStyle::from_taffy_style(Style {
                    display: Display::Flex,
                    align_items: Some(AlignItems::Center),
                    justify_content: Some(JustifyContent::Center),
                    size: TaffySize {
                        width: length(80.0),
                        height: length(30.0),
                    },
                    padding: taffy::prelude::Rect::length(6.0),
                    ..Default::default()
                }),
                ..Default::default()
            },
            labels: ThemePreferenceLabels::default(),
            include_system: true,
            enabled: true,
            action_prefix: Some("theme.preference".to_owned()),
            accessibility_label: None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThemePreferenceButtonNodes {
    pub root: UiNodeId,
    pub system: Option<UiNodeId>,
    pub light: UiNodeId,
    pub dark: UiNodeId,
}

impl ThemePreferenceButtonNodes {
    pub fn node_for(self, preference: ThemePreference) -> Option<UiNodeId> {
        match preference {
            ThemePreference::System => self.system,
            ThemePreference::Light => Some(self.light),
            ThemePreference::Dark => Some(self.dark),
        }
    }
}

pub fn theme_preference_buttons(
    document: &mut UiDocument,
    parent: UiNodeId,
    name: impl Into<String>,
    current: ThemePreference,
    options: ThemePreferenceButtonsOptions,
) -> ThemePreferenceButtonNodes {
    let name = name.into();
    let root = document.add_child(
        parent,
        UiNode::container(
            name.clone(),
            UiNodeStyle {
                layout: options.layout.style.clone(),
                clip: ClipBehavior::Clip,
                ..Default::default()
            },
        )
        .with_accessibility(
            AccessibilityMeta::new(AccessibilityRole::Group).label(
                options
                    .accessibility_label
                    .clone()
                    .unwrap_or_else(|| "Theme preference".to_owned()),
            ),
        ),
    );

    let system = options.include_system.then(|| {
        add_theme_preference_button(
            document,
            root,
            &name,
            ThemePreference::System,
            current,
            &options,
        )
    });
    let light = add_theme_preference_button(
        document,
        root,
        &name,
        ThemePreference::Light,
        current,
        &options,
    );
    let dark = add_theme_preference_button(
        document,
        root,
        &name,
        ThemePreference::Dark,
        current,
        &options,
    );

    ThemePreferenceButtonNodes {
        root,
        system,
        light,
        dark,
    }
}

fn add_theme_preference_button(
    document: &mut UiDocument,
    parent: UiNodeId,
    name: &str,
    preference: ThemePreference,
    current: ThemePreference,
    options: &ThemePreferenceButtonsOptions,
) -> UiNodeId {
    let label = options.labels.label(preference).to_owned();
    let mut button_options = options.button_options.clone();
    button_options.enabled = options.enabled;
    button_options.action = options.action_for(preference);
    button_options.accessibility_label = Some(format!("{label} theme"));
    toggle_button(
        document,
        parent,
        format!("{name}.{}", preference.as_str()),
        label,
        current == preference,
        button_options,
    )
}

#[derive(Debug, Clone)]
pub struct ThemePreferenceSwitchOptions {
    pub switch_options: ToggleSwitchOptions,
    pub labels: ThemePreferenceLabels,
    pub enabled: bool,
    pub action: Option<WidgetActionBinding>,
    pub accessibility_label: Option<String>,
}

impl ThemePreferenceSwitchOptions {
    pub fn with_switch_options(mut self, options: ToggleSwitchOptions) -> Self {
        self.switch_options = options;
        self
    }

    pub fn with_labels(mut self, labels: ThemePreferenceLabels) -> Self {
        self.labels = labels;
        self
    }

    pub const fn enabled(mut self, enabled: bool) -> Self {
        self.enabled = enabled;
        self
    }

    pub fn with_action(mut self, action: impl Into<WidgetActionBinding>) -> Self {
        self.action = Some(action.into());
        self
    }

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

impl Default for ThemePreferenceSwitchOptions {
    fn default() -> Self {
        Self {
            switch_options: ToggleSwitchOptions::default(),
            labels: ThemePreferenceLabels::default(),
            enabled: true,
            action: Some(WidgetActionBinding::action("theme.preference.dark")),
            accessibility_label: None,
        }
    }
}

pub fn theme_preference_switch(
    document: &mut UiDocument,
    parent: UiNodeId,
    name: impl Into<String>,
    current: ThemePreference,
    options: ThemePreferenceSwitchOptions,
) -> UiNodeId {
    let name = name.into();
    let mut switch_options = options.switch_options;
    switch_options.enabled = options.enabled;
    switch_options.action = options.action;
    switch_options.accessibility_label = Some(
        options
            .accessibility_label
            .unwrap_or_else(|| options.labels.switch.clone()),
    );
    toggle_switch(
        document,
        parent,
        name,
        options.labels.switch,
        if current.is_dark() {
            ToggleValue::On
        } else {
            ToggleValue::Off
        },
        switch_options,
    )
}

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

    #[test]
    fn theme_preference_buttons_build_segmented_choices() {
        let mut document = UiDocument::new(root_style(320.0, 120.0));
        let root = document.root;
        let nodes = theme_preference_buttons(
            &mut document,
            root,
            "theme",
            ThemePreference::Dark,
            ThemePreferenceButtonsOptions::default(),
        );

        assert!(nodes.system.is_some());
        assert_eq!(document.node(nodes.root).children.len(), 3);
        assert_eq!(
            document.node(nodes.dark).action.as_ref(),
            Some(&WidgetActionBinding::action("theme.preference.dark"))
        );
        assert!(document
            .node(nodes.dark)
            .accessibility
            .as_ref()
            .is_some_and(|accessibility| accessibility.pressed == Some(true)));
    }

    #[test]
    fn theme_preference_switch_maps_dark_to_checked_switch() {
        let mut document = UiDocument::new(root_style(320.0, 120.0));
        let root = document.root;
        let switch = theme_preference_switch(
            &mut document,
            root,
            "dark-theme",
            ThemePreference::Dark,
            ThemePreferenceSwitchOptions::default(),
        );

        assert_eq!(
            document.node(switch).action.as_ref(),
            Some(&WidgetActionBinding::action("theme.preference.dark"))
        );
        assert!(
            document
                .node(switch)
                .accessibility
                .as_ref()
                .is_some_and(
                    |accessibility| accessibility.checked == Some(AccessibilityChecked::True)
                )
        );
    }
}