Skip to main content

cranpose_ui/widgets/
button.rs

1//! Button widget implementation
2
3#![allow(non_snake_case)]
4
5use std::{cell::RefCell, rc::Rc};
6
7use cranpose_core::NodeId;
8use cranpose_ui_layout::{HorizontalAlignment, LinearArrangement};
9
10use crate::{
11    composable, interaction::MutableInteractionSource, layout::policies::FlexMeasurePolicy,
12    modifier::Modifier, widgets::Layout,
13};
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 cranpose_core::{Composition, MemoryApplier};
89
90    use super::*;
91
92    #[test]
93    fn default_button_spec_has_no_interaction_source() {
94        let spec = ButtonSpec::default();
95
96        assert!(spec.interaction_source.is_none());
97    }
98
99    #[test]
100    fn button_spec_builder_preserves_interaction_source() {
101        let composition = Composition::new(MemoryApplier::new());
102        let source = MutableInteractionSource::with_runtime(composition.runtime_handle());
103        let spec = ButtonSpec::new().interaction_source(source);
104
105        assert_eq!(spec.interaction_source, Some(source));
106    }
107}