Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 13 kB View raw
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}