Another project
1
fork

Configure Feed

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

feat(ui): frame ctx, interact declarations

Lewis: May this revision serve well! <lu5a@proton.me>

+343 -1
+322
crates/bone-ui/src/frame.rs
··· 1 + use std::sync::Arc; 2 + 3 + use crate::focus::FocusManager; 4 + use crate::hit_test::{HitFrame, HitItem, HitState, Interaction, Sense, ZLayer}; 5 + use crate::hotkey::{ActionId, HotkeyScopes, HotkeyTable, KeyChord}; 6 + use crate::input::{InputSnapshot, KeyEvent}; 7 + use crate::layout::LayoutRect; 8 + use crate::theme::Theme; 9 + use crate::widget_id::WidgetId; 10 + 11 + #[derive(Copy, Clone, Debug, PartialEq)] 12 + pub struct InteractDeclaration { 13 + pub id: WidgetId, 14 + pub rect: LayoutRect, 15 + pub sense: Sense, 16 + pub z: ZLayer, 17 + pub disabled: bool, 18 + pub focusable: bool, 19 + pub active: bool, 20 + } 21 + 22 + impl InteractDeclaration { 23 + #[must_use] 24 + pub const fn new(id: WidgetId, rect: LayoutRect, sense: Sense) -> Self { 25 + Self { 26 + id, 27 + rect, 28 + sense, 29 + z: ZLayer::BASE, 30 + disabled: false, 31 + focusable: false, 32 + active: false, 33 + } 34 + } 35 + 36 + #[must_use] 37 + pub const fn at_z(self, z: ZLayer) -> Self { 38 + Self { z, ..self } 39 + } 40 + 41 + #[must_use] 42 + pub const fn disabled(self, disabled: bool) -> Self { 43 + Self { disabled, ..self } 44 + } 45 + 46 + #[must_use] 47 + pub const fn focusable(self, focusable: bool) -> Self { 48 + Self { focusable, ..self } 49 + } 50 + 51 + #[must_use] 52 + pub const fn active(self, active: bool) -> Self { 53 + Self { active, ..self } 54 + } 55 + } 56 + 57 + pub struct FrameCtx<'a> { 58 + pub theme: Arc<Theme>, 59 + pub input: &'a mut InputSnapshot, 60 + pub focus: &'a mut FocusManager, 61 + pub hotkeys: &'a HotkeyTable, 62 + pub hits: &'a mut HitFrame, 63 + pub previous: &'a HitState, 64 + } 65 + 66 + impl<'a> FrameCtx<'a> { 67 + #[must_use] 68 + pub fn new( 69 + theme: Arc<Theme>, 70 + input: &'a mut InputSnapshot, 71 + focus: &'a mut FocusManager, 72 + hotkeys: &'a HotkeyTable, 73 + hits: &'a mut HitFrame, 74 + previous: &'a HitState, 75 + ) -> Self { 76 + focus.begin_frame(); 77 + focus.observe_input(input); 78 + Self { 79 + theme, 80 + input, 81 + focus, 82 + hotkeys, 83 + hits, 84 + previous, 85 + } 86 + } 87 + 88 + pub fn interact(&mut self, declaration: InteractDeclaration) -> Interaction { 89 + if !declaration.disabled { 90 + self.focus.register_focusable(declaration.id); 91 + if declaration.focusable { 92 + self.focus.register_tab_stop(declaration.id); 93 + } 94 + } 95 + self.hits.push(HitItem { 96 + id: declaration.id, 97 + rect: declaration.rect, 98 + sense: declaration.sense, 99 + z: declaration.z, 100 + disabled: declaration.disabled, 101 + active: declaration.active, 102 + }); 103 + let interaction = self.previous.interaction(declaration.id); 104 + if interaction.click() && declaration.focusable && !declaration.disabled { 105 + self.focus.request_focus(declaration.id); 106 + } 107 + interaction 108 + } 109 + 110 + pub fn dispatch_hotkeys(&mut self, scopes: &HotkeyScopes) -> Vec<ActionId> { 111 + let table = self.hotkeys; 112 + let pending = std::mem::take(&mut self.input.keys_pressed); 113 + let (matched, remaining) = pending.into_iter().fold( 114 + (Vec::new(), Vec::new()), 115 + |(mut matched, mut remaining), event: KeyEvent| { 116 + match table.dispatch(KeyChord::from(event), scopes) { 117 + Some(action) => matched.push(action), 118 + None => remaining.push(event), 119 + } 120 + (matched, remaining) 121 + }, 122 + ); 123 + self.input.keys_pressed = remaining; 124 + matched 125 + } 126 + } 127 + 128 + impl Drop for FrameCtx<'_> { 129 + fn drop(&mut self) { 130 + self.focus.end_frame(); 131 + } 132 + } 133 + 134 + #[cfg(test)] 135 + mod tests { 136 + use core::num::NonZeroU32; 137 + use core::time::Duration; 138 + use std::sync::Arc; 139 + 140 + use super::{FrameCtx, InteractDeclaration}; 141 + use crate::focus::FocusManager; 142 + use crate::hit_test::{HitFrame, HitState, Sense, resolve}; 143 + use crate::hotkey::{ 144 + ActionId, HotkeyBinding, HotkeyScope, HotkeyScopes, HotkeyTable, KeyChord, 145 + }; 146 + use crate::input::{ 147 + FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, PointerButton, 148 + PointerButtonMask, PointerSample, 149 + }; 150 + use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 151 + use crate::theme::Theme; 152 + use crate::widget_id::{WidgetId, WidgetKey}; 153 + 154 + fn global_scope() -> HotkeyScopes { 155 + HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]) 156 + } 157 + 158 + fn rect() -> LayoutRect { 159 + LayoutRect::new( 160 + LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 161 + LayoutSize::new(LayoutPx::new(50.0), LayoutPx::new(50.0)), 162 + ) 163 + } 164 + 165 + fn id(name: &'static str) -> WidgetId { 166 + WidgetId::ROOT.child(WidgetKey::new(name)) 167 + } 168 + 169 + fn action(n: u32) -> ActionId { 170 + let Some(nz) = NonZeroU32::new(n) else { 171 + panic!("test action id must be non-zero"); 172 + }; 173 + ActionId::new(nz) 174 + } 175 + 176 + #[test] 177 + fn interact_registers_tab_stop_when_focusable() { 178 + let theme = Arc::new(Theme::light()); 179 + let mut focus = FocusManager::new(); 180 + let hotkeys = HotkeyTable::new(); 181 + let mut hits = HitFrame::new(); 182 + let prev = HitState::new(); 183 + let mut input = InputSnapshot::idle(FrameInstant::ZERO); 184 + { 185 + let mut frame = 186 + FrameCtx::new(theme, &mut input, &mut focus, &hotkeys, &mut hits, &prev); 187 + let _ = frame.interact( 188 + InteractDeclaration::new(id("button"), rect(), Sense::INTERACTIVE).focusable(true), 189 + ); 190 + } 191 + assert_eq!(focus.tab_stops().len(), 1); 192 + assert!(hits.items().iter().any(|item| item.id == id("button"))); 193 + } 194 + 195 + #[test] 196 + fn dispatch_hotkeys_consumes_pressed_chord() { 197 + let chord = KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL); 198 + let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new( 199 + chord, 200 + HotkeyScope::Global, 201 + action(42), 202 + )]) else { 203 + panic!("registration must succeed"); 204 + }; 205 + let mut input = InputSnapshot::idle(FrameInstant::ZERO); 206 + input.keys_pressed.push(KeyEvent::new( 207 + KeyCode::Char(KeyChar::from_char('s')), 208 + ModifierMask::CTRL, 209 + )); 210 + let mut focus = FocusManager::new(); 211 + let mut hits = HitFrame::new(); 212 + let prev = HitState::new(); 213 + let actions = { 214 + let mut frame = FrameCtx::new( 215 + Arc::new(Theme::light()), 216 + &mut input, 217 + &mut focus, 218 + &table, 219 + &mut hits, 220 + &prev, 221 + ); 222 + frame.dispatch_hotkeys(&global_scope()) 223 + }; 224 + assert_eq!(actions, vec![action(42)]); 225 + assert!(input.keys_pressed.is_empty()); 226 + } 227 + 228 + #[test] 229 + fn dispatch_hotkeys_leaves_unmatched_keys() { 230 + let table = HotkeyTable::new(); 231 + let mut input = InputSnapshot::idle(FrameInstant::ZERO); 232 + let event = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL); 233 + input.keys_pressed.push(event); 234 + let mut focus = FocusManager::new(); 235 + let mut hits = HitFrame::new(); 236 + let prev = HitState::new(); 237 + let actions = { 238 + let mut frame = FrameCtx::new( 239 + Arc::new(Theme::light()), 240 + &mut input, 241 + &mut focus, 242 + &table, 243 + &mut hits, 244 + &prev, 245 + ); 246 + frame.dispatch_hotkeys(&global_scope()) 247 + }; 248 + assert!(actions.is_empty()); 249 + assert_eq!(input.keys_pressed, vec![event]); 250 + } 251 + 252 + #[test] 253 + fn end_to_end_press_release_routes_through_resolve() { 254 + let theme = Arc::new(Theme::light()); 255 + let mut focus = FocusManager::new(); 256 + let hotkeys = HotkeyTable::new(); 257 + let mut hits = HitFrame::new(); 258 + let mut state = HitState::new(); 259 + let mut press = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(0))); 260 + press.pointer = Some(PointerSample::new(LayoutPos::new( 261 + LayoutPx::new(10.0), 262 + LayoutPx::new(10.0), 263 + ))); 264 + press.buttons_pressed = PointerButtonMask::just(PointerButton::Primary); 265 + 266 + { 267 + let mut frame = FrameCtx::new( 268 + theme.clone(), 269 + &mut press, 270 + &mut focus, 271 + &hotkeys, 272 + &mut hits, 273 + &state, 274 + ); 275 + let _ = frame.interact( 276 + InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true), 277 + ); 278 + } 279 + state = resolve(&state, &hits, &press, focus.focused()); 280 + assert!(state.interaction(id("btn")).pressed()); 281 + 282 + hits.clear(); 283 + let mut release = 284 + InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(40))); 285 + release.pointer = Some(PointerSample::new(LayoutPos::new( 286 + LayoutPx::new(15.0), 287 + LayoutPx::new(15.0), 288 + ))); 289 + release.buttons_released = PointerButtonMask::just(PointerButton::Primary); 290 + 291 + { 292 + let mut frame = FrameCtx::new( 293 + theme.clone(), 294 + &mut release, 295 + &mut focus, 296 + &hotkeys, 297 + &mut hits, 298 + &state, 299 + ); 300 + let _ = frame.interact( 301 + InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true), 302 + ); 303 + } 304 + state = resolve(&state, &hits, &release, focus.focused()); 305 + assert!(state.interaction(id("btn")).click()); 306 + 307 + hits.clear(); 308 + let mut idle = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(80))); 309 + { 310 + let mut frame = 311 + FrameCtx::new(theme, &mut idle, &mut focus, &hotkeys, &mut hits, &state); 312 + let _ = frame.interact( 313 + InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true), 314 + ); 315 + } 316 + assert_eq!( 317 + focus.focused(), 318 + Some(id("btn")), 319 + "click on focusable widget auto-focuses next frame", 320 + ); 321 + } 322 + }
+21 -1
crates/bone-ui/src/lib.rs
··· 1 + pub mod focus; 2 + pub mod frame; 3 + pub mod hit_test; 4 + pub mod hotkey; 5 + pub mod input; 1 6 pub mod layout; 2 7 pub mod theme; 3 8 mod widget_id; 4 9 10 + pub use focus::{ 11 + FocusManager, FocusRequest, FocusScopeId, FocusScopeKind, InputModality, RovingDirection, 12 + }; 13 + pub use frame::{FrameCtx, InteractDeclaration}; 14 + pub use hit_test::{ 15 + ClickRecord, HitFrame, HitItem, HitState, Interaction, InteractionState, PointerCapture, 16 + PointerCaptureStack, PressedRecord, Sense, ZLayer, resolve, 17 + }; 18 + pub use hotkey::{ 19 + ActionId, HotkeyBinding, HotkeyScope, HotkeyScopes, HotkeyTable, HotkeyTableError, KeyChord, 20 + }; 21 + pub use input::{ 22 + ClickCount, DoubleClickWindow, DragThreshold, FrameInstant, InputSnapshot, KeyChar, KeyCode, 23 + KeyEvent, ModifierMask, NamedKey, PointerButton, PointerButtonMask, PointerSample, 24 + }; 5 25 pub use theme::{ 6 26 BlurRadius, Border, CadColors, Color, ColorError, Colors, Easing, ElevationLevel, 7 27 ElevationScale, FontFace, FontSize, FontWeight, LetterSpacing, LineHeight, Motion, MotionToken, 8 28 Radius, RadiusScale, Scale12, Shadow, ShadowOffset, Spacing, SpacingScale, Step12, StrokeWidth, 9 29 SurfaceLevel, Theme, ThemeMode, Typography, TypographyRole, 10 30 }; 11 - pub use widget_id::WidgetId; 31 + pub use widget_id::{WidgetId, WidgetKey};