Skip to main content

cranpose_ui/widgets/
button.rs

1//! Button widget implementation
2
3#![allow(non_snake_case)]
4
5use crate::composable;
6use crate::interaction::MutableInteractionSource;
7use crate::layout::policies::FlexMeasurePolicy;
8use crate::modifier::Modifier;
9use crate::widgets::Layout;
10use cranpose_core::NodeId;
11use cranpose_ui_layout::{HorizontalAlignment, LinearArrangement};
12use std::cell::RefCell;
13use std::rc::Rc;
14
15#[derive(Clone, Debug, Default, PartialEq)]
16pub struct ButtonSpec {
17    pub interaction_source: Option<MutableInteractionSource>,
18}
19
20impl ButtonSpec {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    pub fn interaction_source(mut self, interaction_source: MutableInteractionSource) -> Self {
26        self.interaction_source = Some(interaction_source);
27        self
28    }
29}
30
31fn button_modifier<F>(modifier: Modifier, spec: ButtonSpec, on_click: F) -> Modifier
32where
33    F: FnMut() + 'static,
34{
35    let on_click_rc: Rc<RefCell<dyn FnMut()>> = Rc::new(RefCell::new(on_click));
36    let modifier = if let Some(interaction_source) = spec.interaction_source {
37        modifier.press_interaction_source(interaction_source)
38    } else {
39        modifier
40    };
41
42    modifier.clickable(move |_point| {
43        (on_click_rc.borrow_mut())();
44    })
45}
46
47/// A clickable button with a background and content.
48///
49/// # When to use
50/// Use this to trigger an action when clicked. The button serves as a container
51/// for other composables (typically `Text`).
52///
53/// # Arguments
54///
55/// * `modifier` - Modifiers to apply to the button container.
56/// * `spec` - Configuration for button behavior.
57/// * `on_click` - The callback to execute when the button is clicked.
58/// * `content` - The content to display inside the button (e.g., `Text` or `Icon`).
59///
60/// # Example
61///
62/// ```rust,ignore
63/// Button(
64///     Modifier::padding(8.0),
65///     ButtonSpec::default(),
66///     || println!("Clicked!"),
67///     || Text("Click Me", Modifier::empty())
68/// );
69/// ```
70#[composable]
71pub fn Button<F, G>(modifier: Modifier, spec: ButtonSpec, on_click: F, content: G) -> NodeId
72where
73    F: FnMut() + 'static,
74    G: FnMut() + 'static,
75{
76    Layout(
77        button_modifier(modifier, spec, on_click),
78        FlexMeasurePolicy::column(
79            LinearArrangement::Center,
80            HorizontalAlignment::CenterHorizontally,
81        ),
82        content,
83    )
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use cranpose_core::{Composition, MemoryApplier};
90
91    #[test]
92    fn default_button_spec_has_no_interaction_source() {
93        let spec = ButtonSpec::default();
94
95        assert!(spec.interaction_source.is_none());
96    }
97
98    #[test]
99    fn button_spec_builder_preserves_interaction_source() {
100        let composition = Composition::new(MemoryApplier::new());
101        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
102        let spec = ButtonSpec::new().interaction_source(source.clone());
103
104        assert_eq!(spec.interaction_source, Some(source));
105    }
106}