operad 6.0.0

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

#[derive(Debug, Clone)]
pub struct ButtonOptions {
    pub layout: LayoutStyle,
    pub visual: UiVisual,
    pub hovered_visual: Option<UiVisual>,
    pub pressed_visual: Option<UiVisual>,
    pub pressed_hovered_visual: Option<UiVisual>,
    pub focused_visual: Option<UiVisual>,
    pub disabled_visual: Option<UiVisual>,
    pub text_style: TextStyle,
    pub leading_image: Option<ImageContent>,
    pub image_size: UiSize,
    pub image_shader: Option<ShaderEffect>,
    pub shader: Option<ShaderEffect>,
    pub animation: Option<AnimationMachine>,
    pub enabled: bool,
    pub pressed: bool,
    pub focused: bool,
    pub action: Option<WidgetActionBinding>,
    pub accessibility_label: Option<String>,
    pub accessibility_hint: Option<String>,
}

impl ButtonOptions {
    pub fn new(layout: impl Into<LayoutStyle>) -> Self {
        let layout = layout.into();
        Self {
            layout,
            ..Default::default()
        }
    }

    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 with_command(mut self, command: impl Into<CommandId>) -> Self {
        self.action = Some(WidgetActionBinding::command(command));
        self
    }
}

impl Default for ButtonOptions {
    fn default() -> Self {
        Self {
            layout: LayoutStyle::from_taffy_style(Style {
                display: Display::Flex,
                align_items: Some(AlignItems::Center),
                justify_content: Some(JustifyContent::Center),
                size: TaffySize {
                    width: Dimension::auto(),
                    height: Dimension::auto(),
                },
                ..Default::default()
            }),
            visual: UiVisual::panel(
                ColorRgba::new(36, 42, 52, 255),
                Some(StrokeStyle::new(ColorRgba::new(74, 85, 104, 255), 1.0)),
                4.0,
            ),
            hovered_visual: Some(UiVisual::panel(
                ColorRgba::new(56, 70, 86, 255),
                Some(StrokeStyle::new(ColorRgba::new(138, 164, 194, 255), 1.0)),
                4.0,
            )),
            pressed_visual: Some(UiVisual::panel(
                ColorRgba::new(22, 30, 40, 255),
                Some(StrokeStyle::new(ColorRgba::new(78, 104, 134, 255), 1.0)),
                4.0,
            )),
            pressed_hovered_visual: Some(UiVisual::panel(
                ColorRgba::new(42, 58, 74, 255),
                Some(StrokeStyle::new(ColorRgba::new(150, 184, 220, 255), 1.0)),
                4.0,
            )),
            focused_visual: Some(UiVisual::panel(
                ColorRgba::new(36, 42, 52, 255),
                Some(StrokeStyle::new(ColorRgba::new(120, 170, 230, 255), 1.5)),
                4.0,
            )),
            disabled_visual: Some(UiVisual::panel(
                ColorRgba::new(30, 34, 40, 180),
                Some(StrokeStyle::new(ColorRgba::new(64, 72, 84, 180), 1.0)),
                4.0,
            )),
            text_style: TextStyle::default(),
            leading_image: None,
            image_size: UiSize::new(18.0, 18.0),
            image_shader: None,
            shader: None,
            animation: None,
            enabled: true,
            pressed: false,
            focused: false,
            action: None,
            accessibility_label: None,
            accessibility_hint: None,
        }
    }
}

impl ButtonOptions {
    fn resolved_visual(&self) -> UiVisual {
        if !self.enabled {
            self.disabled_visual.unwrap_or(self.visual)
        } else if self.pressed {
            self.pressed_visual.unwrap_or(self.visual)
        } else if self.focused {
            self.focused_visual.unwrap_or(self.visual)
        } else {
            self.visual
        }
    }

    fn interaction_visuals(&self) -> InteractionVisuals {
        InteractionVisuals::new(self.resolved_visual())
            .hovered(self.hovered_visual.unwrap_or(self.visual))
            .pressed(self.pressed_visual.unwrap_or(self.visual))
            .pressed_hovered(
                self.pressed_hovered_visual
                    .or(self.pressed_visual)
                    .unwrap_or(self.visual),
            )
            .focused(self.focused_visual.unwrap_or(self.visual))
            .disabled(self.disabled_visual.unwrap_or(self.visual))
    }

    fn resolved_visual_for_interaction(
        &self,
        hovered: bool,
        pressed: bool,
        focused: bool,
    ) -> UiVisual {
        self.interaction_visuals()
            .resolve(self.enabled, hovered, pressed, focused)
    }
}

impl ButtonOptions {
    fn update_document_interaction_visual(&self, document: &mut UiDocument, button: UiNodeId) {
        document.set_node_interaction_visuals(button, self.interaction_visuals());
        let hovered = document.focus.hovered == Some(button);
        let pressed = document.focus.pressed == Some(button) || self.pressed;
        let focused = document.focus.focused == Some(button) || self.focused;
        let visual = self.resolved_visual_for_interaction(hovered, pressed, focused);
        document.set_node_visual(button, visual);
    }
}

pub fn button(
    document: &mut UiDocument,
    parent: UiNodeId,
    name: impl Into<String>,
    label: impl Into<String>,
    options: ButtonOptions,
) -> UiNodeId {
    let name = name.into();
    let label = label.into();
    let mut layout = options.layout.style.clone();
    layout.display = Display::Flex;
    layout.flex_direction = FlexDirection::Row;
    layout.align_items = Some(AlignItems::Center);
    layout.justify_content = Some(JustifyContent::Center);
    let accessibility_label = options
        .accessibility_label
        .clone()
        .unwrap_or_else(|| label.clone());
    let mut accessibility = AccessibilityMeta::new(AccessibilityRole::Button)
        .label(accessibility_label)
        .pressed(options.pressed)
        .action(AccessibilityAction::new("activate", "Activate"));
    if let Some(hint) = options.accessibility_hint.clone() {
        accessibility = accessibility.hint(hint);
    }
    if options.enabled {
        accessibility = accessibility.focusable();
    } else {
        accessibility = accessibility.disabled();
    }
    let mut node = UiNode::container(
        name.clone(),
        UiNodeStyle {
            layout,
            clip: ClipBehavior::Clip,
            ..Default::default()
        },
    )
    .with_input(if options.enabled {
        InputBehavior::BUTTON
    } else {
        InputBehavior::NONE
    })
    .with_interaction_visuals(options.interaction_visuals())
    .with_accessibility(accessibility);
    if let Some(shader) = options.shader.clone() {
        node = node.with_shader(shader);
    }
    if let Some(animation) = options.animation.clone() {
        node = node.with_animation(animation);
    }
    if let Some(action) = options.action.clone() {
        node = node.with_action(action);
    }
    let button = document.add_child(parent, node);
    options.update_document_interaction_visual(document, button);
    if let Some(image) = options.leading_image {
        let mut image_node = UiNode::image(
            format!("{name}.image"),
            image,
            LayoutStyle::from_taffy_style(Style {
                size: TaffySize {
                    width: length(options.image_size.width),
                    height: length(options.image_size.height),
                },
                margin: taffy::prelude::Rect {
                    right: taffy::prelude::LengthPercentageAuto::length(6.0),
                    ..taffy::prelude::Rect::length(0.0)
                },
                ..Default::default()
            }),
        )
        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Image).label(label.clone()));
        if let Some(shader) = options.image_shader {
            image_node = image_node.with_shader(shader);
        }
        document.add_child(button, image_node);
    }
    document.add_child(
        button,
        UiNode::text(
            format!("{name}.label"),
            label,
            options.text_style,
            LayoutStyle::from_taffy_style(Style {
                size: TaffySize {
                    width: Dimension::auto(),
                    height: Dimension::auto(),
                },
                ..Default::default()
            }),
        ),
    );
    button
}

pub fn button_actions_from_input_result(
    document: &UiDocument,
    button: UiNodeId,
    options: &ButtonOptions,
    result: &UiInputResult,
) -> WidgetActionQueue {
    let mut queue = WidgetActionQueue::new();
    push_button_input_result_actions(&mut queue, document, button, options, result);
    queue
}

pub fn push_button_input_result_actions<'a>(
    queue: &'a mut WidgetActionQueue,
    document: &UiDocument,
    button: UiNodeId,
    options: &ButtonOptions,
    result: &UiInputResult,
) -> &'a mut WidgetActionQueue {
    let Some(clicked) = result.clicked else {
        return queue;
    };
    if !document.node_is_descendant_or_self(button, clicked)
        || !action_target_enabled(document, button)
    {
        return queue;
    }
    if let Some(binding) = options.action.clone() {
        queue.push(WidgetAction::pointer_activate(button, binding, 1));
    }
    queue
}

pub fn button_actions_from_key_event(
    document: &UiDocument,
    button: UiNodeId,
    options: &ButtonOptions,
    event: &UiInputEvent,
) -> WidgetActionQueue {
    let mut queue = WidgetActionQueue::new();
    push_button_key_event_actions(&mut queue, document, button, options, event);
    queue
}

pub fn push_button_key_event_actions<'a>(
    queue: &'a mut WidgetActionQueue,
    document: &UiDocument,
    button: UiNodeId,
    options: &ButtonOptions,
    event: &UiInputEvent,
) -> &'a mut WidgetActionQueue {
    let UiInputEvent::Key { key, modifiers } = event else {
        return queue;
    };
    if document.focus.focused != Some(button) || !action_target_enabled(document, button) {
        return queue;
    }
    if let Some(binding) = options.action.clone() {
        queue.push_key_activation(button, binding, *key, *modifiers);
    }
    queue
}

pub fn button_actions_from_gesture_event(
    document: &UiDocument,
    button: UiNodeId,
    options: &ButtonOptions,
    event: &GestureEvent,
) -> WidgetActionQueue {
    let mut queue = WidgetActionQueue::new();
    push_button_gesture_event_actions(&mut queue, document, button, options, event);
    queue
}

pub fn push_button_gesture_event_actions<'a>(
    queue: &'a mut WidgetActionQueue,
    document: &UiDocument,
    button: UiNodeId,
    options: &ButtonOptions,
    event: &GestureEvent,
) -> &'a mut WidgetActionQueue {
    let GestureEvent::Click(click) = event else {
        return queue;
    };
    if click.button != PointerButton::Primary
        || !document.node_is_descendant_or_self(button, click.target)
    {
        return queue;
    }
    if !action_target_enabled(document, button) {
        return queue;
    }
    if let Some(binding) = options.action.clone() {
        queue.push(WidgetAction::pointer_activate(button, binding, click.count));
    }
    queue
}