operad 8.0.0

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

#[derive(Debug, Clone)]
pub struct ToggleSwitchOptions {
    pub layout: LayoutStyle,
    pub track_visual: UiVisual,
    pub on_track_visual: UiVisual,
    pub disabled_track_visual: Option<UiVisual>,
    pub thumb_visual: UiVisual,
    pub disabled_thumb_visual: Option<UiVisual>,
    pub text_style: TextStyle,
    pub enabled: bool,
    pub action: Option<WidgetActionBinding>,
    pub accessibility_label: Option<String>,
    pub accessibility_hint: Option<String>,
}

impl Default for ToggleSwitchOptions {
    fn default() -> Self {
        Self {
            layout: LayoutStyle::from_taffy_style(Style {
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                align_items: Some(AlignItems::Center),
                size: TaffySize {
                    width: Dimension::auto(),
                    height: length(30.0),
                },
                ..Default::default()
            }),
            track_visual: UiVisual::panel(ColorRgba::new(42, 49, 58, 255), None, 11.0),
            on_track_visual: UiVisual::panel(ColorRgba::new(21, 92, 78, 255), None, 11.0),
            disabled_track_visual: Some(UiVisual::panel(
                ColorRgba::new(35, 39, 45, 160),
                None,
                11.0,
            )),
            thumb_visual: UiVisual::panel(
                ColorRgba::new(235, 240, 247, 255),
                Some(StrokeStyle::new(ColorRgba::new(79, 93, 113, 255), 1.0)),
                9.0,
            ),
            disabled_thumb_visual: Some(UiVisual::panel(
                ColorRgba::new(150, 158, 170, 180),
                Some(StrokeStyle::new(ColorRgba::new(81, 90, 104, 180), 1.0)),
                9.0,
            )),
            text_style: TextStyle::default(),
            enabled: true,
            action: None,
            accessibility_label: None,
            accessibility_hint: None,
        }
    }
}

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

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

pub fn toggle_switch(
    document: &mut UiDocument,
    parent: UiNodeId,
    name: impl Into<String>,
    label_text: impl Into<String>,
    value: ToggleValue,
    options: ToggleSwitchOptions,
) -> UiNodeId {
    let name = name.into();
    let label_text = label_text.into();
    let mut accessibility = AccessibilityMeta::new(AccessibilityRole::Switch)
        .label(
            options
                .accessibility_label
                .clone()
                .unwrap_or_else(|| label_text.clone()),
        )
        .value(value.label())
        .action(AccessibilityAction::new("toggle", "Toggle"));
    accessibility = if value.is_mixed() {
        accessibility.mixed()
    } else {
        accessibility.checked(value.is_on())
    };
    if let Some(hint) = options.accessibility_hint.clone() {
        accessibility = accessibility.hint(hint);
    }
    if options.enabled {
        accessibility = accessibility.focusable();
    } else {
        accessibility = accessibility.disabled();
    }

    let root_layout = options.layout.style.clone();
    let mut root_node = UiNode::container(
        name.clone(),
        UiNodeStyle {
            layout: root_layout.clone(),
            clip: ClipBehavior::Clip,
            ..Default::default()
        },
    )
    .with_input(if options.enabled {
        InputBehavior::BUTTON
    } else {
        InputBehavior::NONE
    })
    .with_accessibility(accessibility);
    if let Some(action) = options.action.clone() {
        root_node = root_node.with_action(action);
    }
    let root = document.add_child(parent, root_node);

    let track_visual = if !options.enabled {
        options
            .disabled_track_visual
            .unwrap_or(options.track_visual)
    } else if value.is_on() {
        options.on_track_visual
    } else {
        options.track_visual
    };
    let thumb_visual = if options.enabled {
        options.thumb_visual
    } else {
        options
            .disabled_thumb_visual
            .unwrap_or(options.thumb_visual)
    };
    let track_layout = Style {
        display: Display::Flex,
        flex_direction: FlexDirection::Row,
        align_items: Some(AlignItems::Center),
        size: TaffySize {
            width: length(44.0),
            height: length(22.0),
        },
        padding: taffy::prelude::Rect::length(2.0),
        margin: taffy::prelude::Rect {
            right: taffy::prelude::LengthPercentageAuto::length(8.0),
            ..taffy::prelude::Rect::length(0.0)
        },
        flex_shrink: 0.0,
        ..Default::default()
    };
    let track = document.add_child(
        root,
        UiNode::container(
            format!("{name}.track"),
            LayoutStyle::from_taffy_style(track_layout.clone()),
        )
        .with_visual(track_visual),
    );
    if value.is_on() {
        document.add_child(
            track,
            UiNode::container(
                format!("{name}.track.before_thumb"),
                LayoutStyle::new().with_flex_grow(1.0).with_height(1.0),
            ),
        );
    }
    document.add_child(
        track,
        UiNode::container(
            format!("{name}.thumb"),
            LayoutStyle::size(18.0, 18.0).with_flex_shrink(0.0),
        )
        .with_visual(thumb_visual),
    );
    if !value.is_on() {
        document.add_child(
            track,
            UiNode::container(
                format!("{name}.track.after_thumb"),
                LayoutStyle::new().with_flex_grow(1.0).with_height(1.0),
            ),
        );
    }
    if !label_text.is_empty() {
        let label_style = single_line_text_style(options.text_style);
        let label = label(
            document,
            root,
            format!("{name}.label"),
            label_text,
            label_style,
            LayoutStyle::new(),
        );
        publish_inline_intrinsic_size(
            document,
            root,
            vec![label],
            inline_intrinsic_base_size(&root_layout, &[&track_layout], 2),
        );
    }
    root
}

pub fn toggle_switch_actions_from_input_result(
    document: &UiDocument,
    toggle: UiNodeId,
    value: ToggleValue,
    options: &ToggleSwitchOptions,
    result: &UiInputResult,
) -> WidgetActionQueue {
    let mut queue = WidgetActionQueue::new();
    if result
        .clicked
        .is_some_and(|target| document.node_is_descendant_or_self(toggle, target))
        && action_target_enabled(document, toggle)
    {
        if let Some(binding) = options.action.clone() {
            queue.select(toggle, binding, value.toggled().is_on());
        }
    }
    queue
}

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

    #[test]
    fn toggle_switch_builds_switch_with_track_thumb_and_label() {
        let mut document = UiDocument::new(root_style(240.0, 120.0));
        let root = document.root;
        let node = toggle_switch(
            &mut document,
            root,
            "autosave",
            "Autosave",
            ToggleValue::On,
            ToggleSwitchOptions::default().with_action("autosave.toggle"),
        );

        let accessibility = document.node(node).accessibility.as_ref().unwrap();
        assert_eq!(accessibility.role, AccessibilityRole::Switch);
        assert_eq!(accessibility.checked, Some(AccessibilityChecked::True));
        assert_eq!(document.node(node).children.len(), 2);
        assert_eq!(
            document.node(node).action.as_ref(),
            Some(&WidgetActionBinding::action("autosave.toggle"))
        );
    }
}