1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Test that Button widget properly integrates with clickable modifier system
//!
//! This test verifies that the Button widget internally applies Modifier.clickable()
//! so that click handlers are properly wired into the pointer input system.
use cranpose_core::MutableState;
use cranpose_macros::composable;
use cranpose_testing::ComposeTestRule;
use cranpose_ui::*;
#[composable]
fn simple_button_app(clicked_count: MutableState<i32>) {
Column(
Modifier::empty().padding(20.0),
ColumnSpec::default(),
move || {
Text(
format!("Clicks: {}", clicked_count.get()),
Modifier::empty().padding(8.0),
TextStyle::default(),
);
Button(
Modifier::empty().padding(10.0),
ButtonSpec::default(),
{
let count = clicked_count;
move || {
count.set(count.get() + 1);
}
},
|| {
Text(
"Click me",
Modifier::empty().padding(4.0),
TextStyle::default(),
);
},
);
},
);
}
#[test]
fn test_button_creates_valid_composition() {
let _app_context = cranpose_ui::AppContext::new();
let _app_context_scope = _app_context.enter_scope();
_app_context.enter(cranpose_ui::reset_render_state_for_tests);
// This test verifies that Button widgets can be created within a composition.
// The Button widget should internally apply Modifier.clickable() to connect
// the on_click handler to the pointer input system.
let mut rule = ComposeTestRule::new();
let runtime = rule.runtime_handle();
let clicked_count = MutableState::with_runtime(0, runtime.clone());
// Set content with a button
rule.set_content({
let count = clicked_count;
move || {
simple_button_app(count);
}
})
.expect("initial render succeeds");
// Verify initial state
assert_eq!(
clicked_count.get(),
0,
"Button should not have been clicked yet"
);
// The composition should have created nodes for Column, Text, Button, ButtonSpec, and Button's Text child
let node_count = rule.applier_mut().len();
assert!(
node_count >= 4,
"Should have at least 4 nodes (Column, Text, Button, ButtonSpec, Button's Text)"
);
// In a real app, clicking would:
// 1. Window system generates mouse event
// 2. AppShell calls hit_test() on the rendered scene
// 3. Scene finds HitRegion containing the button's click_action
// 4. HitRegion.dispatch() invokes the click_action handler
// 5. The handler increments the counter
// 6. State change triggers recomposition
//
// The critical fix this test validates:
// - Button now internally applies Modifier.clickable()
// - This ensures click handlers are extracted into HitRegions during rendering
// - Before this fix, Button stored on_click but never connected it to modifiers
}
#[composable]
fn multi_button_app(button1_clicks: MutableState<i32>, button2_clicks: MutableState<i32>) {
Column(
Modifier::empty().padding(20.0),
ColumnSpec::default(),
move || {
Text(
format!("Button 1 clicks: {}", button1_clicks.get()),
Modifier::empty().padding(8.0),
TextStyle::default(),
);
Button(
Modifier::empty().padding(10.0),
ButtonSpec::default(),
{
let clicks = button1_clicks;
move || {
clicks.set(clicks.get() + 1);
}
},
|| {
Text(
"Button 1",
Modifier::empty().padding(4.0),
TextStyle::default(),
);
},
);
Text(
format!("Button 2 clicks: {}", button2_clicks.get()),
Modifier::empty().padding(8.0),
TextStyle::default(),
);
Button(
Modifier::empty().padding(10.0),
ButtonSpec::default(),
{
let clicks = button2_clicks;
move || {
clicks.set(clicks.get() + 10);
}
},
|| {
Text(
"Button 2",
Modifier::empty().padding(4.0),
TextStyle::default(),
);
},
);
},
);
}
#[test]
fn test_multiple_buttons_in_composition() {
let _app_context = cranpose_ui::AppContext::new();
let _app_context_scope = _app_context.enter_scope();
_app_context.enter(cranpose_ui::reset_render_state_for_tests);
// Test that multiple buttons can coexist and each has its own click handler
let mut rule = ComposeTestRule::new();
let runtime = rule.runtime_handle();
let button1_clicks = MutableState::with_runtime(0, runtime.clone());
let button2_clicks = MutableState::with_runtime(0, runtime.clone());
rule.set_content({
let clicks1 = button1_clicks;
let clicks2 = button2_clicks;
move || {
multi_button_app(clicks1, clicks2);
}
})
.expect("initial render succeeds");
// Verify initial state
assert_eq!(button1_clicks.get(), 0);
assert_eq!(button2_clicks.get(), 0);
// Both buttons should be created successfully
// Should have: Column, 2 Texts for click counts, 2 Buttons, 2 Texts inside buttons = 7 nodes minimum
let node_count = rule.applier_mut().len();
assert!(
node_count >= 7,
"Should have at least 7 nodes for the two button app"
);
}