Skip to main content

cranpose_ui/widgets/
button.rs

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