Another project
0

Configure Feed

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

at main 11 kB View raw
1use serde::Serialize; 2 3use crate::a11y::{AccessNode, Role, Toggled}; 4use crate::frame::{FrameCtx, InteractDeclaration}; 5use crate::hit_test::{Interaction, Sense}; 6use crate::layout::LayoutRect; 7use crate::strings::StringKey; 8use crate::widget_id::WidgetId; 9 10use super::keys::take_activation; 11use super::paint::{GlyphMark, WidgetPaint}; 12use super::visuals::{Indicator, IndicatorMark, push_indicator}; 13 14#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] 15pub enum CheckboxState { 16 Unchecked, 17 Checked, 18 Indeterminate, 19} 20 21impl CheckboxState { 22 #[must_use] 23 pub const fn next(self) -> Self { 24 match self { 25 Self::Unchecked => Self::Checked, 26 Self::Checked | Self::Indeterminate => Self::Unchecked, 27 } 28 } 29 30 #[must_use] 31 pub const fn is_active(self) -> bool { 32 matches!(self, Self::Checked | Self::Indeterminate) 33 } 34} 35 36#[derive(Copy, Clone, Debug, PartialEq)] 37pub struct Checkbox { 38 pub id: WidgetId, 39 pub rect: LayoutRect, 40 pub label: StringKey, 41 pub state: CheckboxState, 42 pub disabled: bool, 43} 44 45impl Checkbox { 46 #[must_use] 47 pub const fn new( 48 id: WidgetId, 49 rect: LayoutRect, 50 label: StringKey, 51 state: CheckboxState, 52 ) -> Self { 53 Self { 54 id, 55 rect, 56 label, 57 state, 58 disabled: false, 59 } 60 } 61 62 #[must_use] 63 pub const fn disabled(self, disabled: bool) -> Self { 64 Self { disabled, ..self } 65 } 66} 67 68#[derive(Clone, Debug, PartialEq)] 69pub struct CheckboxResponse { 70 pub interaction: Interaction, 71 pub state: CheckboxState, 72 pub toggled: bool, 73 pub paint: Vec<WidgetPaint>, 74} 75 76#[must_use] 77pub fn show_checkbox(ctx: &mut FrameCtx<'_>, checkbox: Checkbox) -> CheckboxResponse { 78 let interactive = !checkbox.disabled; 79 let toggled_state = match checkbox.state { 80 CheckboxState::Unchecked => Toggled::False, 81 CheckboxState::Checked => Toggled::True, 82 CheckboxState::Indeterminate => Toggled::Mixed, 83 }; 84 let interaction = ctx.interact( 85 InteractDeclaration::new(checkbox.id, checkbox.rect, Sense::INTERACTIVE) 86 .focusable(interactive) 87 .disabled(!interactive) 88 .active(checkbox.state.is_active()) 89 .a11y( 90 AccessNode::new(Role::CheckBox) 91 .with_label(checkbox.label) 92 .with_disabled(!interactive) 93 .with_toggled(toggled_state), 94 ), 95 ); 96 let live_focused = ctx.is_focused(checkbox.id); 97 let toggled = 98 interactive && (interaction.click() || (live_focused && take_activation(ctx.input))); 99 let next_state = if toggled { 100 checkbox.state.next() 101 } else { 102 checkbox.state 103 }; 104 let paint = build_paint(ctx, &checkbox, interaction, live_focused, next_state); 105 CheckboxResponse { 106 interaction, 107 state: next_state, 108 toggled, 109 paint, 110 } 111} 112 113fn build_paint( 114 ctx: &FrameCtx<'_>, 115 checkbox: &Checkbox, 116 interaction: Interaction, 117 live_focused: bool, 118 state: CheckboxState, 119) -> Vec<WidgetPaint> { 120 let mark = match state { 121 CheckboxState::Checked => Some(IndicatorMark::Check), 122 CheckboxState::Indeterminate => Some(IndicatorMark::Glyph(GlyphMark::Indeterminate)), 123 CheckboxState::Unchecked => None, 124 }; 125 let mut paint = Vec::new(); 126 push_indicator( 127 ctx, 128 &mut paint, 129 Indicator { 130 rect: checkbox.rect, 131 label: checkbox.label, 132 mark, 133 active: state.is_active(), 134 disabled: checkbox.disabled, 135 radius: ctx.theme().radius.sm, 136 }, 137 interaction, 138 live_focused, 139 ); 140 paint 141} 142 143#[cfg(test)] 144mod tests { 145 use std::sync::Arc; 146 147 use super::{Checkbox, CheckboxState, show_checkbox}; 148 use crate::focus::FocusManager; 149 use crate::frame::FrameCtx; 150 use crate::hit_test::{HitFrame, HitState, resolve}; 151 use crate::hotkey::HotkeyTable; 152 use crate::input::{ 153 FrameInstant, InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey, PointerButton, 154 PointerButtonMask, PointerSample, 155 }; 156 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 157 use crate::strings::StringKey; 158 use crate::strings::StringTable; 159 use crate::theme::Theme; 160 use crate::widget_id::{WidgetId, WidgetKey}; 161 162 const LABEL: StringKey = StringKey::new("checkbox.label"); 163 164 fn rect() -> LayoutRect { 165 LayoutRect::new( 166 LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 167 LayoutSize::new(LayoutPx::new(48.0), LayoutPx::new(20.0)), 168 ) 169 } 170 171 fn id_widget() -> WidgetId { 172 WidgetId::ROOT.child(WidgetKey::new("checkbox")) 173 } 174 175 #[test] 176 fn next_cycles_through_states() { 177 assert_eq!(CheckboxState::Unchecked.next(), CheckboxState::Checked); 178 assert_eq!(CheckboxState::Checked.next(), CheckboxState::Unchecked); 179 assert_eq!( 180 CheckboxState::Indeterminate.next(), 181 CheckboxState::Unchecked 182 ); 183 } 184 185 #[test] 186 fn space_key_when_focused_toggles() { 187 let theme = Arc::new(Theme::light()); 188 let table = HotkeyTable::new(); 189 let mut hits = HitFrame::new(); 190 let state = HitState::new(); 191 let mut focus = FocusManager::new(); 192 focus.register_focusable(id_widget()); 193 focus.request_focus(id_widget()); 194 focus.end_frame(); 195 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 196 input.keys_pressed.push(KeyEvent::new( 197 KeyCode::Named(NamedKey::Space), 198 ModifierMask::NONE, 199 )); 200 let checkbox = Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Unchecked); 201 let response = { 202 let mut shaper = bone_text::Shaper::new(); 203 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 204 let mut ctx = FrameCtx::new( 205 theme, 206 &mut input, 207 &mut focus, 208 &table, 209 StringTable::empty(), 210 &mut hits, 211 &state, 212 &mut a11y, 213 &mut shaper, 214 ); 215 show_checkbox(&mut ctx, checkbox) 216 }; 217 assert!(response.toggled); 218 assert_eq!(response.state, CheckboxState::Checked); 219 } 220 221 #[test] 222 fn pointer_click_through_three_frames_flips_state() { 223 let theme = Arc::new(Theme::light()); 224 let table = HotkeyTable::new(); 225 let mut focus = FocusManager::new(); 226 let mut hits = HitFrame::new(); 227 let mut state = HitState::new(); 228 let mut current = CheckboxState::Unchecked; 229 [press_snap(), release_snap(), idle_snap()] 230 .into_iter() 231 .for_each(|mut input| { 232 hits.clear(); 233 let checkbox = Checkbox::new(id_widget(), rect(), LABEL, current); 234 let response = { 235 let mut shaper = bone_text::Shaper::new(); 236 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 237 let mut ctx = FrameCtx::new( 238 theme.clone(), 239 &mut input, 240 &mut focus, 241 &table, 242 StringTable::empty(), 243 &mut hits, 244 &state, 245 &mut a11y, 246 &mut shaper, 247 ); 248 show_checkbox(&mut ctx, checkbox) 249 }; 250 current = response.state; 251 state = resolve(&state, &hits, &input, focus.focused()); 252 }); 253 assert_eq!(current, CheckboxState::Checked); 254 } 255 256 #[test] 257 fn indeterminate_checkbox_paints_indeterminate_mark() { 258 let theme = Arc::new(Theme::light()); 259 let table = HotkeyTable::new(); 260 let mut focus = FocusManager::new(); 261 let mut hits = HitFrame::new(); 262 let state = HitState::new(); 263 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 264 let checkbox = Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Indeterminate); 265 let response = { 266 let mut shaper = bone_text::Shaper::new(); 267 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 268 let mut ctx = FrameCtx::new( 269 theme, 270 &mut input, 271 &mut focus, 272 &table, 273 StringTable::empty(), 274 &mut hits, 275 &state, 276 &mut a11y, 277 &mut shaper, 278 ); 279 show_checkbox(&mut ctx, checkbox) 280 }; 281 let has_indeterminate = response.paint.iter().any(|p| { 282 matches!( 283 p, 284 super::WidgetPaint::Mark { 285 kind: super::GlyphMark::Indeterminate, 286 .. 287 } 288 ) 289 }); 290 assert!(has_indeterminate); 291 } 292 293 #[test] 294 fn disabled_checkbox_ignores_keys() { 295 let theme = Arc::new(Theme::light()); 296 let table = HotkeyTable::new(); 297 let mut hits = HitFrame::new(); 298 let state = HitState::new(); 299 let mut focus = FocusManager::new(); 300 focus.register_focusable(id_widget()); 301 focus.request_focus(id_widget()); 302 focus.end_frame(); 303 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 304 let event = KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE); 305 input.keys_pressed.push(event); 306 let checkbox = 307 Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Unchecked).disabled(true); 308 let response = { 309 let mut shaper = bone_text::Shaper::new(); 310 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 311 let mut ctx = FrameCtx::new( 312 theme, 313 &mut input, 314 &mut focus, 315 &table, 316 StringTable::empty(), 317 &mut hits, 318 &state, 319 &mut a11y, 320 &mut shaper, 321 ); 322 show_checkbox(&mut ctx, checkbox) 323 }; 324 assert!(!response.toggled); 325 assert_eq!(input.keys_pressed, vec![event]); 326 } 327 328 fn press_snap() -> InputSnapshot { 329 let mut s = InputSnapshot::idle(FrameInstant::ZERO); 330 s.pointer = Some(PointerSample::new(LayoutPos::new( 331 LayoutPx::new(10.0), 332 LayoutPx::new(10.0), 333 ))); 334 s.buttons_pressed = PointerButtonMask::just(PointerButton::Primary); 335 s 336 } 337 338 fn release_snap() -> InputSnapshot { 339 let mut s = InputSnapshot::idle(FrameInstant::ZERO); 340 s.pointer = Some(PointerSample::new(LayoutPos::new( 341 LayoutPx::new(12.0), 342 LayoutPx::new(12.0), 343 ))); 344 s.buttons_released = PointerButtonMask::just(PointerButton::Primary); 345 s 346 } 347 348 fn idle_snap() -> InputSnapshot { 349 let mut s = InputSnapshot::idle(FrameInstant::ZERO); 350 s.pointer = Some(PointerSample::new(LayoutPos::new( 351 LayoutPx::new(12.0), 352 LayoutPx::new(12.0), 353 ))); 354 s 355 } 356}