Another project
1use crate::a11y::{AccessNode, Role, Toggled};
2use crate::frame::{FrameCtx, InteractDeclaration};
3use crate::hit_test::{Interaction, Sense};
4use crate::layout::LayoutRect;
5use crate::strings::StringKey;
6use crate::widget_id::WidgetId;
7
8use super::button::ButtonState;
9use super::keys::take_activation;
10use super::paint::WidgetPaint;
11use super::visuals::{Indicator, push_indicator};
12
13#[derive(Copy, Clone, Debug, PartialEq)]
14pub struct ToggleButton {
15 pub id: WidgetId,
16 pub rect: LayoutRect,
17 pub label: StringKey,
18 pub state: ButtonState,
19 pub on: bool,
20}
21
22impl ToggleButton {
23 #[must_use]
24 pub const fn new(id: WidgetId, rect: LayoutRect, label: StringKey, on: bool) -> Self {
25 Self {
26 id,
27 rect,
28 label,
29 on,
30 state: ButtonState::Idle,
31 }
32 }
33
34 #[must_use]
35 pub const fn with_state(self, state: ButtonState) -> Self {
36 Self { state, ..self }
37 }
38}
39
40#[derive(Clone, Debug, PartialEq)]
41pub struct ToggleButtonResponse {
42 pub interaction: Interaction,
43 pub on: bool,
44 pub toggled: bool,
45 pub paint: Vec<WidgetPaint>,
46}
47
48#[must_use]
49pub fn show_toggle_button(ctx: &mut FrameCtx<'_>, toggle: ToggleButton) -> ToggleButtonResponse {
50 let interactive = toggle.state.is_interactive();
51 let interaction = ctx.interact(
52 InteractDeclaration::new(toggle.id, toggle.rect, Sense::INTERACTIVE)
53 .focusable(interactive)
54 .disabled(!interactive)
55 .active(toggle.on)
56 .a11y(
57 AccessNode::new(Role::Switch)
58 .with_label(toggle.label)
59 .with_disabled(!interactive)
60 .with_toggled(if toggle.on {
61 Toggled::True
62 } else {
63 Toggled::False
64 }),
65 ),
66 );
67 let live_focused = ctx.is_focused(toggle.id);
68 let toggled =
69 interactive && (interaction.click() || (live_focused && take_activation(ctx.input)));
70 let next_on = if toggled { !toggle.on } else { toggle.on };
71 let disabled = matches!(toggle.state, ButtonState::Disabled);
72 let mut paint = Vec::new();
73 push_indicator(
74 ctx,
75 &mut paint,
76 Indicator {
77 rect: toggle.rect,
78 label: toggle.label,
79 mark: None,
80 active: next_on,
81 disabled,
82 radius: ctx.theme().radius.sm,
83 },
84 interaction,
85 live_focused,
86 );
87 ToggleButtonResponse {
88 interaction,
89 on: next_on,
90 toggled,
91 paint,
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use std::sync::Arc;
98
99 use super::{ToggleButton, show_toggle_button};
100 use crate::focus::FocusManager;
101 use crate::frame::FrameCtx;
102 use crate::hit_test::{HitFrame, HitState, resolve};
103 use crate::hotkey::HotkeyTable;
104 use crate::input::{
105 FrameInstant, InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey, PointerButton,
106 PointerButtonMask, PointerSample,
107 };
108 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
109 use crate::strings::StringKey;
110 use crate::strings::StringTable;
111 use crate::theme::Theme;
112 use crate::widget_id::{WidgetId, WidgetKey};
113
114 const LABEL: StringKey = StringKey::new("toggle.snap_to_grid");
115
116 fn rect() -> LayoutRect {
117 LayoutRect::new(
118 LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
119 LayoutSize::new(LayoutPx::new(64.0), LayoutPx::new(28.0)),
120 )
121 }
122
123 fn id_widget() -> WidgetId {
124 WidgetId::ROOT.child(WidgetKey::new("toggle"))
125 }
126
127 fn cycle_pointer_click(start_on: bool) -> Vec<bool> {
128 let theme = Arc::new(Theme::light());
129 let mut focus = FocusManager::new();
130 let table = HotkeyTable::new();
131 let mut hits = HitFrame::new();
132 let mut state = HitState::new();
133 let mut on = start_on;
134 let mut history = Vec::new();
135 let step = |snap: &mut InputSnapshot,
136 focus: &mut FocusManager,
137 hits: &mut HitFrame,
138 state: &mut HitState,
139 on: &mut bool,
140 history: &mut Vec<bool>| {
141 hits.clear();
142 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, *on);
143 let response = {
144 let mut shaper = bone_text::Shaper::new();
145 let mut a11y = crate::a11y::AccessTreeBuilder::new();
146 let mut ctx = FrameCtx::new(
147 theme.clone(),
148 snap,
149 focus,
150 &table,
151 StringTable::empty(),
152 hits,
153 state,
154 &mut a11y,
155 &mut shaper,
156 );
157 show_toggle_button(&mut ctx, toggle)
158 };
159 *on = response.on;
160 history.push(*on);
161 *state = resolve(state, hits, snap, focus.focused());
162 };
163
164 let mut press = InputSnapshot::idle(FrameInstant::ZERO);
165 press.pointer = Some(PointerSample::new(LayoutPos::new(
166 LayoutPx::new(10.0),
167 LayoutPx::new(10.0),
168 )));
169 press.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
170 step(
171 &mut press,
172 &mut focus,
173 &mut hits,
174 &mut state,
175 &mut on,
176 &mut history,
177 );
178
179 let mut release = InputSnapshot::idle(FrameInstant::ZERO);
180 release.pointer = Some(PointerSample::new(LayoutPos::new(
181 LayoutPx::new(15.0),
182 LayoutPx::new(15.0),
183 )));
184 release.buttons_released = PointerButtonMask::just(PointerButton::Primary);
185 step(
186 &mut release,
187 &mut focus,
188 &mut hits,
189 &mut state,
190 &mut on,
191 &mut history,
192 );
193
194 let mut idle = InputSnapshot::idle(FrameInstant::ZERO);
195 idle.pointer = Some(PointerSample::new(LayoutPos::new(
196 LayoutPx::new(15.0),
197 LayoutPx::new(15.0),
198 )));
199 step(
200 &mut idle,
201 &mut focus,
202 &mut hits,
203 &mut state,
204 &mut on,
205 &mut history,
206 );
207 history
208 }
209
210 #[test]
211 fn pointer_click_flips_off_to_on() {
212 let history = cycle_pointer_click(false);
213 assert_eq!(history, vec![false, false, true]);
214 }
215
216 #[test]
217 fn pointer_click_flips_on_to_off() {
218 let history = cycle_pointer_click(true);
219 assert_eq!(history, vec![true, true, false]);
220 }
221
222 #[test]
223 fn space_key_toggles_when_focused() {
224 let theme = Arc::new(Theme::light());
225 let table = HotkeyTable::new();
226 let mut hits = HitFrame::new();
227 let state = HitState::new();
228 let mut focus = FocusManager::new();
229 focus.register_focusable(id_widget());
230 focus.request_focus(id_widget());
231 focus.end_frame();
232 let mut input = InputSnapshot::idle(FrameInstant::ZERO);
233 input.keys_pressed.push(KeyEvent::new(
234 KeyCode::Named(NamedKey::Space),
235 ModifierMask::NONE,
236 ));
237 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, false);
238 let response = {
239 let mut shaper = bone_text::Shaper::new();
240 let mut a11y = crate::a11y::AccessTreeBuilder::new();
241 let mut ctx = FrameCtx::new(
242 theme,
243 &mut input,
244 &mut focus,
245 &table,
246 StringTable::empty(),
247 &mut hits,
248 &state,
249 &mut a11y,
250 &mut shaper,
251 );
252 show_toggle_button(&mut ctx, toggle)
253 };
254 assert!(response.toggled);
255 assert!(response.on);
256 }
257
258 #[test]
259 fn disabled_toggle_does_not_flip() {
260 let theme = Arc::new(Theme::light());
261 let mut focus = FocusManager::new();
262 let table = HotkeyTable::new();
263 let mut hits = HitFrame::new();
264 let mut state = HitState::new();
265 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, false)
266 .with_state(super::ButtonState::Disabled);
267 let history: Vec<bool> = [
268 press_snap(),
269 release_snap(),
270 InputSnapshot::idle(FrameInstant::ZERO),
271 ]
272 .into_iter()
273 .map(|mut snap| {
274 hits.clear();
275 let response = {
276 let mut shaper = bone_text::Shaper::new();
277 let mut a11y = crate::a11y::AccessTreeBuilder::new();
278 let mut ctx = FrameCtx::new(
279 theme.clone(),
280 &mut snap,
281 &mut focus,
282 &table,
283 StringTable::empty(),
284 &mut hits,
285 &state,
286 &mut a11y,
287 &mut shaper,
288 );
289 show_toggle_button(&mut ctx, toggle)
290 };
291 state = resolve(&state, &hits, &snap, focus.focused());
292 response.on
293 })
294 .collect();
295 assert!(history.iter().all(|on| !on), "disabled never flips");
296 }
297
298 fn press_snap() -> InputSnapshot {
299 let mut s = InputSnapshot::idle(FrameInstant::ZERO);
300 s.pointer = Some(PointerSample::new(LayoutPos::new(
301 LayoutPx::new(10.0),
302 LayoutPx::new(10.0),
303 )));
304 s.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
305 s
306 }
307
308 fn release_snap() -> InputSnapshot {
309 let mut s = InputSnapshot::idle(FrameInstant::ZERO);
310 s.pointer = Some(PointerSample::new(LayoutPos::new(
311 LayoutPx::new(15.0),
312 LayoutPx::new(15.0),
313 )));
314 s.buttons_released = PointerButtonMask::just(PointerButton::Primary);
315 s
316 }
317
318 fn surface_fill(paint: &[super::WidgetPaint]) -> Option<crate::theme::Color> {
319 paint.iter().find_map(|p| match p {
320 super::WidgetPaint::Surface { fill, .. } => Some(*fill),
321 _ => None,
322 })
323 }
324
325 fn focused_at_widget() -> FocusManager {
326 let mut focus = FocusManager::new();
327 focus.register_focusable(id_widget());
328 focus.request_focus(id_widget());
329 focus.end_frame();
330 focus
331 }
332
333 #[test]
334 fn paint_reflects_next_on_not_initial() {
335 let theme = Arc::new(Theme::light());
336 let table = HotkeyTable::new();
337 let prelit = {
338 let mut focus = focused_at_widget();
339 let mut hits = HitFrame::new();
340 let state = HitState::new();
341 let mut input = InputSnapshot::idle(FrameInstant::ZERO);
342 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, true);
343 let response = {
344 let mut shaper = bone_text::Shaper::new();
345 let mut a11y = crate::a11y::AccessTreeBuilder::new();
346 let mut ctx = FrameCtx::new(
347 theme.clone(),
348 &mut input,
349 &mut focus,
350 &table,
351 StringTable::empty(),
352 &mut hits,
353 &state,
354 &mut a11y,
355 &mut shaper,
356 );
357 show_toggle_button(&mut ctx, toggle)
358 };
359 surface_fill(&response.paint)
360 };
361 let toggled = {
362 let mut focus = focused_at_widget();
363 let mut hits = HitFrame::new();
364 let state = HitState::new();
365 let mut input = InputSnapshot::idle(FrameInstant::ZERO);
366 input.keys_pressed.push(KeyEvent::new(
367 KeyCode::Named(NamedKey::Space),
368 ModifierMask::NONE,
369 ));
370 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, false);
371 let response = {
372 let mut shaper = bone_text::Shaper::new();
373 let mut a11y = crate::a11y::AccessTreeBuilder::new();
374 let mut ctx = FrameCtx::new(
375 theme.clone(),
376 &mut input,
377 &mut focus,
378 &table,
379 StringTable::empty(),
380 &mut hits,
381 &state,
382 &mut a11y,
383 &mut shaper,
384 );
385 show_toggle_button(&mut ctx, toggle)
386 };
387 assert!(response.on, "Space flipped toggle on");
388 surface_fill(&response.paint)
389 };
390 assert_eq!(
391 toggled, prelit,
392 "activation frame paints active surface, not stale off-state",
393 );
394 }
395}