Another project
0

Configure Feed

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

at main 34 kB View raw
1use std::collections::{BTreeMap, BTreeSet}; 2 3use serde::{Deserialize, Serialize}; 4 5use crate::input::{ 6 ClickCount, DoubleClickWindow, FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, 7}; 8use crate::layout::{LayoutOffset, LayoutPos, LayoutRect}; 9use crate::widget_id::WidgetId; 10 11#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 12#[serde(transparent)] 13pub struct Sense(u8); 14 15impl Sense { 16 pub const HOVER: Self = Self(1 << 0); 17 pub const PRESS: Self = Self(1 << 1); 18 pub const DRAG: Self = Self(1 << 2); 19 pub const INTERACTIVE: Self = Self(Self::HOVER.0 | Self::PRESS.0); 20 pub const DRAGGABLE: Self = Self(Self::HOVER.0 | Self::PRESS.0 | Self::DRAG.0); 21 22 #[must_use] 23 pub const fn contains(self, other: Self) -> bool { 24 (self.0 & other.0) == other.0 25 } 26} 27 28#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 29#[serde(transparent)] 30pub struct ZLayer(u32); 31 32impl ZLayer { 33 pub const BASE: Self = Self(0); 34 pub const POPUP: Self = Self(100); 35 36 #[must_use] 37 pub const fn new(value: u32) -> Self { 38 Self(value) 39 } 40 41 #[must_use] 42 pub const fn get(self) -> u32 { 43 self.0 44 } 45} 46 47#[derive(Copy, Clone, Debug, PartialEq)] 48pub struct HitItem { 49 pub id: WidgetId, 50 pub rect: LayoutRect, 51 pub sense: Sense, 52 pub z: ZLayer, 53 pub disabled: bool, 54 pub active: bool, 55} 56 57#[derive(Clone, Debug, Default, PartialEq)] 58pub struct HitFrame { 59 items: Vec<HitItem>, 60 seen: BTreeSet<WidgetId>, 61} 62 63impl HitFrame { 64 #[must_use] 65 pub fn new() -> Self { 66 Self::default() 67 } 68 69 pub fn push(&mut self, item: HitItem) { 70 assert!( 71 !item.sense.contains(Sense::DRAG) || item.sense.contains(Sense::PRESS), 72 "Sense::DRAG implies Sense::PRESS", 73 ); 74 assert!( 75 !item.sense.contains(Sense::PRESS) || item.sense.contains(Sense::HOVER), 76 "Sense::PRESS implies Sense::HOVER", 77 ); 78 assert!( 79 self.seen.insert(item.id), 80 "duplicate WidgetId in HitFrame: {:?}", 81 item.id, 82 ); 83 self.items.push(item); 84 } 85 86 #[must_use] 87 pub fn items(&self) -> &[HitItem] { 88 &self.items 89 } 90 91 pub fn clear(&mut self) { 92 self.items.clear(); 93 self.seen.clear(); 94 } 95 96 fn topmost(&self, pos: LayoutPos, required: Sense) -> Option<&HitItem> { 97 self.items 98 .iter() 99 .enumerate() 100 .filter(|(_, item)| item.sense.contains(required) && item.rect.contains(pos)) 101 .max_by_key(|(idx, item)| (item.z, *idx)) 102 .map(|(_, item)| item) 103 } 104 105 fn lookup(&self, id: WidgetId) -> Option<&HitItem> { 106 self.items.iter().find(|item| item.id == id) 107 } 108} 109 110#[derive(Copy, Clone, Debug, PartialEq)] 111pub struct PointerCapture { 112 pub id: WidgetId, 113 pub button: PointerButton, 114} 115 116#[derive(Clone, Debug, Default, PartialEq)] 117pub struct PointerCaptureStack { 118 captures: Vec<PointerCapture>, 119} 120 121impl PointerCaptureStack { 122 #[must_use] 123 pub fn new() -> Self { 124 Self::default() 125 } 126 127 #[must_use] 128 pub fn top(&self) -> Option<PointerCapture> { 129 self.captures.last().copied() 130 } 131 132 pub fn push(&mut self, capture: PointerCapture) { 133 self.captures.push(capture); 134 } 135 136 pub fn release_button(&mut self, button: PointerButton) { 137 self.captures.retain(|c| c.button != button); 138 } 139 140 #[must_use] 141 pub fn captures(&self) -> &[PointerCapture] { 142 &self.captures 143 } 144 145 #[must_use] 146 pub fn is_empty(&self) -> bool { 147 self.captures.is_empty() 148 } 149} 150 151#[derive(Copy, Clone, Debug, PartialEq)] 152pub struct PressedRecord { 153 pub id: WidgetId, 154 pub button: PointerButton, 155 pub origin: LayoutPos, 156 pub started_at: FrameInstant, 157 pub drag_active: bool, 158} 159 160#[derive(Copy, Clone, Debug, PartialEq)] 161pub struct ClickRecord { 162 pub id: WidgetId, 163 pub at: FrameInstant, 164 pub count: ClickCount, 165} 166 167#[derive( 168 Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize, 169)] 170#[serde(transparent)] 171pub struct InteractionState(u16); 172 173impl InteractionState { 174 pub const NONE: Self = Self(0); 175 pub const HOVER: Self = Self(1 << 0); 176 pub const PRESSED: Self = Self(1 << 1); 177 pub const FOCUSED: Self = Self(1 << 2); 178 pub const DISABLED: Self = Self(1 << 3); 179 pub const ACTIVE: Self = Self(1 << 4); 180 pub const CLICK: Self = Self(1 << 5); 181 pub const DOUBLE_CLICK: Self = Self(1 << 6); 182 pub const DRAG_START: Self = Self(1 << 7); 183 pub const DRAG_RELEASE: Self = Self(1 << 8); 184 185 #[must_use] 186 pub const fn contains(self, other: Self) -> bool { 187 (self.0 & other.0) == other.0 188 } 189 190 #[must_use] 191 pub const fn with(self, flag: Self, on: bool) -> Self { 192 if on { 193 Self(self.0 | flag.0) 194 } else { 195 Self(self.0 & !flag.0) 196 } 197 } 198} 199 200#[derive(Copy, Clone, Debug, PartialEq, Default)] 201pub struct Interaction { 202 pub state: InteractionState, 203 pub drag_delta: LayoutOffset, 204 pub pressed_buttons: PointerButtonMask, 205 pub click_button: Option<PointerButton>, 206 pub drag_button: Option<PointerButton>, 207} 208 209impl Interaction { 210 #[must_use] 211 pub const fn idle() -> Self { 212 Self { 213 state: InteractionState::NONE, 214 drag_delta: LayoutOffset::ZERO, 215 pressed_buttons: PointerButtonMask::EMPTY, 216 click_button: None, 217 drag_button: None, 218 } 219 } 220 221 #[must_use] 222 pub const fn hover(self) -> bool { 223 self.state.contains(InteractionState::HOVER) 224 } 225 226 #[must_use] 227 pub const fn pressed(self) -> bool { 228 self.state.contains(InteractionState::PRESSED) 229 } 230 231 #[must_use] 232 pub const fn focused(self) -> bool { 233 self.state.contains(InteractionState::FOCUSED) 234 } 235 236 #[must_use] 237 pub const fn disabled(self) -> bool { 238 self.state.contains(InteractionState::DISABLED) 239 } 240 241 #[must_use] 242 pub const fn active(self) -> bool { 243 self.state.contains(InteractionState::ACTIVE) 244 } 245 246 #[must_use] 247 pub const fn click(self) -> bool { 248 self.state.contains(InteractionState::CLICK) 249 } 250 251 #[must_use] 252 pub const fn double_click(self) -> bool { 253 self.state.contains(InteractionState::DOUBLE_CLICK) 254 } 255 256 #[must_use] 257 pub const fn drag_start(self) -> bool { 258 self.state.contains(InteractionState::DRAG_START) 259 } 260 261 #[must_use] 262 pub const fn drag_release(self) -> bool { 263 self.state.contains(InteractionState::DRAG_RELEASE) 264 } 265} 266 267#[derive(Clone, Debug, Default, PartialEq)] 268pub struct HitState { 269 pub hovered: Option<WidgetId>, 270 pub pressed: BTreeMap<PointerButton, PressedRecord>, 271 pub last_click: BTreeMap<PointerButton, ClickRecord>, 272 pub captures: PointerCaptureStack, 273 pub interactions: BTreeMap<WidgetId, Interaction>, 274 pub last_pointer: Option<LayoutPos>, 275} 276 277impl HitState { 278 #[must_use] 279 pub fn new() -> Self { 280 Self::default() 281 } 282 283 #[must_use] 284 pub fn interaction(&self, id: WidgetId) -> Interaction { 285 self.interactions 286 .get(&id) 287 .copied() 288 .unwrap_or(Interaction::idle()) 289 } 290} 291 292#[derive(Copy, Clone, Debug, PartialEq)] 293struct ButtonEvent { 294 id: WidgetId, 295 button: PointerButton, 296} 297 298#[derive(Copy, Clone, Debug, PartialEq)] 299struct ClickEvent { 300 id: WidgetId, 301 button: PointerButton, 302 count: ClickCount, 303} 304 305#[derive(Default)] 306struct Phase { 307 pressed: BTreeMap<PointerButton, PressedRecord>, 308 captures: PointerCaptureStack, 309 last_click: BTreeMap<PointerButton, ClickRecord>, 310 drag_starts: Vec<ButtonEvent>, 311 drag_releases: Vec<ButtonEvent>, 312 clicks: Vec<ClickEvent>, 313 release_records: Vec<PressedRecord>, 314} 315 316#[must_use] 317pub fn resolve( 318 prev: &HitState, 319 frame: &HitFrame, 320 input: &InputSnapshot, 321 focused: Option<WidgetId>, 322) -> HitState { 323 let pointer = input.pointer.map(|p| p.position); 324 let phase = Phase { 325 pressed: prev.pressed.clone(), 326 captures: prev.captures.clone(), 327 last_click: prev.last_click.clone(), 328 ..Phase::default() 329 }; 330 let phase = apply_press(phase, frame, input, pointer); 331 let phase = apply_drag_start(phase, input, frame, pointer); 332 let phase = apply_release(phase, frame, input, pointer); 333 334 let routed_id = route_pointer(&phase.captures, frame, pointer); 335 let sticky_pointer = pointer.or(prev.last_pointer); 336 let interactions = build_interactions( 337 frame, 338 &InteractionInputs { 339 hovered: routed_id, 340 focused, 341 pressed: &phase.pressed, 342 release_records: &phase.release_records, 343 pointer: sticky_pointer, 344 drag_starts: &phase.drag_starts, 345 drag_releases: &phase.drag_releases, 346 clicks: &phase.clicks, 347 }, 348 ); 349 350 HitState { 351 hovered: routed_id, 352 pressed: phase.pressed, 353 last_click: phase.last_click, 354 captures: phase.captures, 355 interactions, 356 last_pointer: sticky_pointer, 357 } 358} 359 360fn route_pointer( 361 captures: &PointerCaptureStack, 362 frame: &HitFrame, 363 pointer: Option<LayoutPos>, 364) -> Option<WidgetId> { 365 if let Some(cap) = captures.top() 366 && frame.lookup(cap.id).is_some() 367 { 368 return Some(cap.id); 369 } 370 pointer 371 .and_then(|p| frame.topmost(p, Sense::HOVER)) 372 .map(|item| item.id) 373} 374 375fn apply_press( 376 phase: Phase, 377 frame: &HitFrame, 378 input: &InputSnapshot, 379 pointer: Option<LayoutPos>, 380) -> Phase { 381 PointerButton::ALL 382 .iter() 383 .copied() 384 .filter(|button| input.buttons_pressed.contains(*button)) 385 .fold(phase, |mut phase, button| { 386 let target = pointer 387 .and_then(|p| frame.topmost(p, Sense::PRESS)) 388 .filter(|item| !item.disabled); 389 if let Some(item) = target 390 && let Some(p) = pointer 391 { 392 phase.pressed.insert( 393 button, 394 PressedRecord { 395 id: item.id, 396 button, 397 origin: p, 398 started_at: input.frame, 399 drag_active: false, 400 }, 401 ); 402 if item.sense.contains(Sense::DRAG) { 403 phase.captures.push(PointerCapture { 404 id: item.id, 405 button, 406 }); 407 } 408 } 409 phase 410 }) 411} 412 413fn apply_drag_start( 414 phase: Phase, 415 input: &InputSnapshot, 416 frame: &HitFrame, 417 pointer: Option<LayoutPos>, 418) -> Phase { 419 let candidates: Vec<PressedRecord> = phase 420 .pressed 421 .values() 422 .filter(|rec| !rec.drag_active) 423 .filter(|rec| { 424 frame 425 .lookup(rec.id) 426 .is_some_and(|item| item.sense.contains(Sense::DRAG) && !item.disabled) 427 }) 428 .copied() 429 .collect(); 430 candidates.into_iter().fold(phase, |mut phase, rec| { 431 let Some(p) = pointer else { return phase }; 432 let offset = LayoutOffset::between(rec.origin, p); 433 if !input.drag_threshold.exceeded_by(offset) { 434 return phase; 435 } 436 if let Some(record) = phase.pressed.get_mut(&rec.button) { 437 record.drag_active = true; 438 } 439 let already_captured = phase 440 .captures 441 .captures() 442 .iter() 443 .any(|c| c.id == rec.id && c.button == rec.button); 444 if !already_captured { 445 phase.captures.push(PointerCapture { 446 id: rec.id, 447 button: rec.button, 448 }); 449 } 450 phase.drag_starts.push(ButtonEvent { 451 id: rec.id, 452 button: rec.button, 453 }); 454 phase 455 }) 456} 457 458fn apply_release( 459 phase: Phase, 460 frame: &HitFrame, 461 input: &InputSnapshot, 462 pointer: Option<LayoutPos>, 463) -> Phase { 464 PointerButton::ALL 465 .iter() 466 .copied() 467 .filter(|button| input.buttons_released.contains(*button)) 468 .fold(phase, |mut phase, button| { 469 if let Some(rec) = phase.pressed.remove(&button) { 470 phase.release_records.push(rec); 471 if rec.drag_active { 472 phase.drag_releases.push(ButtonEvent { id: rec.id, button }); 473 } else { 474 let pointer_over = pointer.is_some_and(|p| { 475 frame 476 .lookup(rec.id) 477 .is_some_and(|item| item.rect.contains(p)) 478 }); 479 if pointer_over { 480 let count = next_click_count( 481 phase.last_click.get(&button).copied(), 482 rec.id, 483 input.frame, 484 input.double_click_window, 485 ); 486 phase.last_click.insert( 487 button, 488 ClickRecord { 489 id: rec.id, 490 at: input.frame, 491 count, 492 }, 493 ); 494 phase.clicks.push(ClickEvent { 495 id: rec.id, 496 button, 497 count, 498 }); 499 } 500 } 501 } 502 phase.captures.release_button(button); 503 phase 504 }) 505} 506 507struct InteractionInputs<'a> { 508 hovered: Option<WidgetId>, 509 focused: Option<WidgetId>, 510 pressed: &'a BTreeMap<PointerButton, PressedRecord>, 511 release_records: &'a [PressedRecord], 512 pointer: Option<LayoutPos>, 513 drag_starts: &'a [ButtonEvent], 514 drag_releases: &'a [ButtonEvent], 515 clicks: &'a [ClickEvent], 516} 517 518fn build_interactions( 519 frame: &HitFrame, 520 inputs: &InteractionInputs<'_>, 521) -> BTreeMap<WidgetId, Interaction> { 522 frame 523 .items() 524 .iter() 525 .map(|item| (item.id, interaction_for(item, inputs))) 526 .collect() 527} 528 529fn interaction_for(item: &HitItem, inputs: &InteractionInputs<'_>) -> Interaction { 530 let pressed_buttons = inputs 531 .pressed 532 .values() 533 .filter(|rec| rec.id == item.id) 534 .fold(PointerButtonMask::EMPTY, |mask, rec| mask.with(rec.button)); 535 let drag_record = inputs 536 .pressed 537 .values() 538 .find(|rec| rec.id == item.id && rec.drag_active) 539 .copied() 540 .or_else(|| { 541 inputs 542 .release_records 543 .iter() 544 .find(|rec| rec.id == item.id && rec.drag_active) 545 .copied() 546 }); 547 let drag_delta = match (drag_record, inputs.pointer) { 548 (Some(rec), Some(p)) => LayoutOffset::between(rec.origin, p), 549 _ => LayoutOffset::ZERO, 550 }; 551 let drag_start_event = inputs.drag_starts.iter().find(|ev| ev.id == item.id); 552 let drag_button = drag_record 553 .map(|rec| rec.button) 554 .or_else(|| drag_start_event.map(|ev| ev.button)); 555 let click_match = inputs.clicks.iter().find(|ev| ev.id == item.id); 556 let state = InteractionState::NONE 557 .with(InteractionState::HOVER, inputs.hovered == Some(item.id)) 558 .with(InteractionState::FOCUSED, inputs.focused == Some(item.id)) 559 .with(InteractionState::PRESSED, !pressed_buttons.is_empty()) 560 .with(InteractionState::DISABLED, item.disabled) 561 .with(InteractionState::ACTIVE, item.active) 562 .with(InteractionState::CLICK, click_match.is_some()) 563 .with( 564 InteractionState::DOUBLE_CLICK, 565 click_match.is_some_and(|ev| ev.count.is_double()), 566 ) 567 .with(InteractionState::DRAG_START, drag_start_event.is_some()) 568 .with( 569 InteractionState::DRAG_RELEASE, 570 inputs.drag_releases.iter().any(|ev| ev.id == item.id), 571 ); 572 Interaction { 573 state, 574 drag_delta, 575 pressed_buttons, 576 click_button: click_match.map(|ev| ev.button), 577 drag_button, 578 } 579} 580 581fn next_click_count( 582 last: Option<ClickRecord>, 583 id: WidgetId, 584 now: FrameInstant, 585 window: DoubleClickWindow, 586) -> ClickCount { 587 match last { 588 Some(prev) if prev.id == id && window.contains(now.since(prev.at)) => prev.count.next(), 589 _ => ClickCount::SINGLE, 590 } 591} 592 593#[cfg(test)] 594mod tests { 595 use core::time::Duration; 596 597 use super::{HitFrame, HitItem, HitState, Interaction, Sense, ZLayer, resolve}; 598 use crate::input::{ 599 DoubleClickWindow, DragThreshold, FrameInstant, InputSnapshot, PointerButton, 600 PointerButtonMask, PointerSample, 601 }; 602 use crate::layout::{LayoutOffset, LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 603 use crate::widget_id::{WidgetId, WidgetKey}; 604 605 fn id(key: &'static str) -> WidgetId { 606 WidgetId::ROOT.child(WidgetKey::new(key)) 607 } 608 609 fn rect(x: f32, y: f32, w: f32, h: f32) -> LayoutRect { 610 LayoutRect::new( 611 LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)), 612 LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)), 613 ) 614 } 615 616 fn pos(x: f32, y: f32) -> LayoutPos { 617 LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)) 618 } 619 620 fn item(id: WidgetId, rect: LayoutRect, sense: Sense) -> HitItem { 621 HitItem { 622 id, 623 rect, 624 sense, 625 z: ZLayer::BASE, 626 disabled: false, 627 active: false, 628 } 629 } 630 631 fn snap_at(pointer: LayoutPos, frame_ms: u64) -> InputSnapshot { 632 let mut s = 633 InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(frame_ms))); 634 s.pointer = Some(PointerSample::new(pointer)); 635 s 636 } 637 638 fn press_snap(pointer: LayoutPos, frame_ms: u64, button: PointerButton) -> InputSnapshot { 639 let mut s = snap_at(pointer, frame_ms); 640 s.buttons_pressed = PointerButtonMask::just(button); 641 s 642 } 643 644 fn release_snap(pointer: LayoutPos, frame_ms: u64, button: PointerButton) -> InputSnapshot { 645 let mut s = snap_at(pointer, frame_ms); 646 s.buttons_released = PointerButtonMask::just(button); 647 s 648 } 649 650 #[test] 651 fn hover_routes_to_topmost_item() { 652 let mut frame = HitFrame::new(); 653 frame.push(item( 654 id("under"), 655 rect(0.0, 0.0, 100.0, 100.0), 656 Sense::HOVER, 657 )); 658 frame.push(HitItem { 659 z: ZLayer::new(10), 660 ..item(id("over"), rect(20.0, 20.0, 40.0, 40.0), Sense::HOVER) 661 }); 662 let state = resolve(&HitState::new(), &frame, &snap_at(pos(30.0, 30.0), 0), None); 663 assert_eq!(state.hovered, Some(id("over"))); 664 } 665 666 #[test] 667 fn click_fires_on_press_and_release_inside() { 668 let mut frame = HitFrame::new(); 669 frame.push(item( 670 id("btn"), 671 rect(0.0, 0.0, 50.0, 50.0), 672 Sense::INTERACTIVE, 673 )); 674 let press = press_snap(pos(10.0, 10.0), 0, PointerButton::Primary); 675 let after_press = resolve(&HitState::new(), &frame, &press, None); 676 assert!(after_press.interaction(id("btn")).pressed()); 677 assert!(!after_press.interaction(id("btn")).click()); 678 679 let release = release_snap(pos(15.0, 15.0), 50, PointerButton::Primary); 680 let after_release = resolve(&after_press, &frame, &release, None); 681 let i = after_release.interaction(id("btn")); 682 assert!(i.click()); 683 assert!(!i.pressed()); 684 assert_eq!(i.click_button, Some(PointerButton::Primary)); 685 } 686 687 #[test] 688 fn release_outside_does_not_fire_click() { 689 let mut frame = HitFrame::new(); 690 frame.push(item( 691 id("btn"), 692 rect(0.0, 0.0, 50.0, 50.0), 693 Sense::INTERACTIVE, 694 )); 695 let press = press_snap(pos(10.0, 10.0), 0, PointerButton::Primary); 696 let after_press = resolve(&HitState::new(), &frame, &press, None); 697 698 let release = release_snap(pos(200.0, 200.0), 50, PointerButton::Primary); 699 let after_release = resolve(&after_press, &frame, &release, None); 700 assert!(!after_release.interaction(id("btn")).click()); 701 } 702 703 #[test] 704 fn double_click_fires_within_window() { 705 let mut frame = HitFrame::new(); 706 frame.push(item( 707 id("btn"), 708 rect(0.0, 0.0, 50.0, 50.0), 709 Sense::INTERACTIVE, 710 )); 711 let s1 = resolve( 712 &HitState::new(), 713 &frame, 714 &press_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 715 None, 716 ); 717 let s2 = resolve( 718 &s1, 719 &frame, 720 &release_snap(pos(5.0, 5.0), 30, PointerButton::Primary), 721 None, 722 ); 723 assert!(s2.interaction(id("btn")).click()); 724 assert!(!s2.interaction(id("btn")).double_click()); 725 726 let s3 = resolve( 727 &s2, 728 &frame, 729 &press_snap(pos(5.0, 5.0), 60, PointerButton::Primary), 730 None, 731 ); 732 let s4 = resolve( 733 &s3, 734 &frame, 735 &release_snap(pos(5.0, 5.0), 90, PointerButton::Primary), 736 None, 737 ); 738 let i = s4.interaction(id("btn")); 739 assert!(i.click()); 740 assert!(i.double_click()); 741 } 742 743 #[test] 744 fn triple_click_keeps_double_click_flag() { 745 let mut frame = HitFrame::new(); 746 frame.push(item( 747 id("btn"), 748 rect(0.0, 0.0, 50.0, 50.0), 749 Sense::INTERACTIVE, 750 )); 751 let states = (0..3).fold(HitState::new(), |state, n| { 752 let press_ms = n * 60; 753 let release_ms = press_ms + 30; 754 let s = resolve( 755 &state, 756 &frame, 757 &press_snap(pos(5.0, 5.0), press_ms, PointerButton::Primary), 758 None, 759 ); 760 resolve( 761 &s, 762 &frame, 763 &release_snap(pos(5.0, 5.0), release_ms, PointerButton::Primary), 764 None, 765 ) 766 }); 767 let Some(last) = states.last_click.get(&PointerButton::Primary).copied() else { 768 panic!("triple-click leaves a click record"); 769 }; 770 assert_eq!(last.count.get(), 3); 771 assert!(last.count.is_double()); 772 } 773 774 #[test] 775 fn drag_capture_routes_motion_outside_bounds() { 776 let mut frame = HitFrame::new(); 777 frame.push(item( 778 id("handle"), 779 rect(0.0, 0.0, 20.0, 20.0), 780 Sense::DRAGGABLE, 781 )); 782 let s1 = resolve( 783 &HitState::new(), 784 &frame, 785 &press_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 786 None, 787 ); 788 assert_eq!(s1.captures.top().map(|c| c.id), Some(id("handle"))); 789 790 let motion = snap_at(pos(200.0, 200.0), 16); 791 let s2 = resolve(&s1, &frame, &motion, None); 792 let i = s2.interaction(id("handle")); 793 assert!(i.drag_start()); 794 assert_ne!(i.drag_delta, LayoutOffset::ZERO); 795 assert_eq!(i.drag_button, Some(PointerButton::Primary)); 796 assert_eq!(s2.hovered, Some(id("handle"))); 797 } 798 799 #[test] 800 fn drag_release_clears_capture_and_skips_click() { 801 let mut frame = HitFrame::new(); 802 frame.push(item( 803 id("handle"), 804 rect(0.0, 0.0, 20.0, 20.0), 805 Sense::DRAGGABLE, 806 )); 807 let s1 = resolve( 808 &HitState::new(), 809 &frame, 810 &press_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 811 None, 812 ); 813 let motion = snap_at(pos(200.0, 200.0), 16); 814 let s2 = resolve(&s1, &frame, &motion, None); 815 let s3 = resolve( 816 &s2, 817 &frame, 818 &release_snap(pos(220.0, 220.0), 32, PointerButton::Primary), 819 None, 820 ); 821 let i = s3.interaction(id("handle")); 822 assert!(i.drag_release()); 823 assert!(!i.click()); 824 assert!(s3.captures.is_empty()); 825 } 826 827 #[test] 828 fn disabled_item_does_not_press() { 829 let mut frame = HitFrame::new(); 830 frame.push(HitItem { 831 disabled: true, 832 ..item(id("btn"), rect(0.0, 0.0, 50.0, 50.0), Sense::INTERACTIVE) 833 }); 834 let press = press_snap(pos(10.0, 10.0), 0, PointerButton::Primary); 835 let s = resolve(&HitState::new(), &frame, &press, None); 836 let i = s.interaction(id("btn")); 837 assert!(!i.pressed()); 838 assert!(i.disabled()); 839 } 840 841 #[test] 842 fn focused_id_threads_into_interaction() { 843 let mut frame = HitFrame::new(); 844 frame.push(item(id("btn"), rect(0.0, 0.0, 50.0, 50.0), Sense::HOVER)); 845 let s = resolve( 846 &HitState::new(), 847 &frame, 848 &snap_at(pos(10.0, 10.0), 0), 849 Some(id("btn")), 850 ); 851 assert!(s.interaction(id("btn")).focused()); 852 } 853 854 #[test] 855 fn active_flag_threads_from_hit_item() { 856 let mut frame = HitFrame::new(); 857 frame.push(HitItem { 858 active: true, 859 ..item(id("toggle"), rect(0.0, 0.0, 50.0, 50.0), Sense::HOVER) 860 }); 861 let s = resolve(&HitState::new(), &frame, &snap_at(pos(10.0, 10.0), 0), None); 862 assert!(s.interaction(id("toggle")).active()); 863 } 864 865 #[test] 866 fn interaction_idle_default() { 867 let i = Interaction::idle(); 868 assert!(!i.hover()); 869 assert!(!i.click()); 870 assert_eq!(i.drag_delta, LayoutOffset::ZERO); 871 assert!(i.pressed_buttons.is_empty()); 872 assert_eq!(i.click_button, None); 873 assert_eq!(i.drag_button, None); 874 } 875 876 #[test] 877 fn pressed_buttons_records_each_button_held() { 878 let mut frame = HitFrame::new(); 879 frame.push(item( 880 id("btn"), 881 rect(0.0, 0.0, 50.0, 50.0), 882 Sense::INTERACTIVE, 883 )); 884 let mut snap = snap_at(pos(10.0, 10.0), 0); 885 snap.buttons_pressed = 886 PointerButtonMask::just(PointerButton::Primary).with(PointerButton::Secondary); 887 let s = resolve(&HitState::new(), &frame, &snap, None); 888 let i = s.interaction(id("btn")); 889 assert!(i.pressed_buttons.contains(PointerButton::Primary)); 890 assert!(i.pressed_buttons.contains(PointerButton::Secondary)); 891 assert!(!i.pressed_buttons.contains(PointerButton::Middle)); 892 } 893 894 #[test] 895 #[should_panic(expected = "Sense::PRESS implies Sense::HOVER")] 896 fn press_without_hover_panics_on_push() { 897 let mut frame = HitFrame::new(); 898 frame.push(item(id("rogue"), rect(0.0, 0.0, 10.0, 10.0), Sense::PRESS)); 899 } 900 901 #[test] 902 #[should_panic(expected = "duplicate WidgetId in HitFrame")] 903 fn duplicate_widget_id_in_frame_panics() { 904 let mut frame = HitFrame::new(); 905 frame.push(item(id("dup"), rect(0.0, 0.0, 10.0, 10.0), Sense::HOVER)); 906 frame.push(item(id("dup"), rect(20.0, 0.0, 10.0, 10.0), Sense::HOVER)); 907 } 908 909 #[test] 910 fn release_button_clears_all_captures_for_that_button() { 911 let mut stack = super::PointerCaptureStack::new(); 912 stack.push(super::PointerCapture { 913 id: id("ghost"), 914 button: PointerButton::Primary, 915 }); 916 stack.push(super::PointerCapture { 917 id: id("active"), 918 button: PointerButton::Primary, 919 }); 920 stack.push(super::PointerCapture { 921 id: id("middle"), 922 button: PointerButton::Middle, 923 }); 924 stack.release_button(PointerButton::Primary); 925 assert_eq!(stack.captures().len(), 1); 926 assert_eq!(stack.captures()[0].button, PointerButton::Middle); 927 } 928 929 #[test] 930 fn drag_start_skipped_when_widget_disables_mid_press() { 931 let id_h = id("handle"); 932 let mut frame = HitFrame::new(); 933 frame.push(item(id_h, rect(0.0, 0.0, 20.0, 20.0), Sense::DRAGGABLE)); 934 let s1 = resolve( 935 &HitState::new(), 936 &frame, 937 &press_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 938 None, 939 ); 940 let mut disabled_frame = HitFrame::new(); 941 disabled_frame.push(HitItem { 942 disabled: true, 943 ..item(id_h, rect(0.0, 0.0, 20.0, 20.0), Sense::DRAGGABLE) 944 }); 945 let s2 = resolve(&s1, &disabled_frame, &snap_at(pos(80.0, 80.0), 16), None); 946 assert!(!s2.interaction(id_h).drag_start()); 947 } 948 949 #[test] 950 fn double_click_window_default() { 951 assert_eq!( 952 DoubleClickWindow::DEFAULT.duration(), 953 Duration::from_millis(400) 954 ); 955 } 956 957 #[test] 958 fn drag_threshold_default_4_px() { 959 assert_eq!(DragThreshold::DEFAULT.px(), LayoutPx::new(4.0)); 960 } 961 962 #[test] 963 fn secondary_button_press_release_fires_click() { 964 let mut frame = HitFrame::new(); 965 frame.push(item( 966 id("btn"), 967 rect(0.0, 0.0, 50.0, 50.0), 968 Sense::INTERACTIVE, 969 )); 970 let s1 = resolve( 971 &HitState::new(), 972 &frame, 973 &press_snap(pos(10.0, 10.0), 0, PointerButton::Secondary), 974 None, 975 ); 976 assert!(s1.interaction(id("btn")).pressed()); 977 let s2 = resolve( 978 &s1, 979 &frame, 980 &release_snap(pos(10.0, 10.0), 30, PointerButton::Secondary), 981 None, 982 ); 983 let i = s2.interaction(id("btn")); 984 assert!(i.click()); 985 assert_eq!(i.click_button, Some(PointerButton::Secondary)); 986 } 987 988 #[test] 989 fn two_buttons_pressed_in_same_snapshot() { 990 let mut frame = HitFrame::new(); 991 frame.push(item( 992 id("btn"), 993 rect(0.0, 0.0, 50.0, 50.0), 994 Sense::INTERACTIVE, 995 )); 996 let mut snap = snap_at(pos(10.0, 10.0), 0); 997 snap.buttons_pressed = 998 PointerButtonMask::just(PointerButton::Primary).with(PointerButton::Secondary); 999 let s = resolve(&HitState::new(), &frame, &snap, None); 1000 assert_eq!(s.pressed.len(), 2); 1001 assert!(s.pressed.contains_key(&PointerButton::Primary)); 1002 assert!(s.pressed.contains_key(&PointerButton::Secondary)); 1003 assert!(s.interaction(id("btn")).pressed()); 1004 } 1005 1006 #[test] 1007 fn middle_button_drag_records_drag_button() { 1008 let mut frame = HitFrame::new(); 1009 frame.push(item( 1010 id("viewport"), 1011 rect(0.0, 0.0, 200.0, 200.0), 1012 Sense::DRAGGABLE, 1013 )); 1014 let s1 = resolve( 1015 &HitState::new(), 1016 &frame, 1017 &press_snap(pos(50.0, 50.0), 0, PointerButton::Middle), 1018 None, 1019 ); 1020 let motion = snap_at(pos(120.0, 120.0), 16); 1021 let s2 = resolve(&s1, &frame, &motion, None); 1022 let i = s2.interaction(id("viewport")); 1023 assert!(i.drag_start()); 1024 assert_eq!(i.drag_button, Some(PointerButton::Middle)); 1025 } 1026 1027 #[test] 1028 #[should_panic(expected = "Sense::DRAG implies Sense::PRESS")] 1029 fn drag_without_press_panics_on_push() { 1030 let mut frame = HitFrame::new(); 1031 frame.push(item(id("rogue"), rect(0.0, 0.0, 10.0, 10.0), Sense::DRAG)); 1032 } 1033 1034 #[test] 1035 fn drag_delta_falls_back_to_last_pointer_when_pointer_drops() { 1036 let mut frame = HitFrame::new(); 1037 frame.push(item( 1038 id("handle"), 1039 rect(0.0, 0.0, 20.0, 20.0), 1040 Sense::DRAGGABLE, 1041 )); 1042 let s1 = resolve( 1043 &HitState::new(), 1044 &frame, 1045 &press_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 1046 None, 1047 ); 1048 let s2 = resolve(&s1, &frame, &snap_at(pos(80.0, 80.0), 16), None); 1049 assert!(s2.interaction(id("handle")).drag_start()); 1050 1051 let mut idle = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(32))); 1052 idle.pointer = None; 1053 let s3 = resolve(&s2, &frame, &idle, None); 1054 let i = s3.interaction(id("handle")); 1055 assert_ne!(i.drag_delta, LayoutOffset::ZERO); 1056 assert_eq!(s3.last_pointer, Some(pos(80.0, 80.0))); 1057 } 1058 1059 #[test] 1060 fn ghost_capture_falls_through_to_position_routing() { 1061 let mut frame = HitFrame::new(); 1062 frame.push(item( 1063 id("handle"), 1064 rect(0.0, 0.0, 20.0, 20.0), 1065 Sense::DRAGGABLE, 1066 )); 1067 let s1 = resolve( 1068 &HitState::new(), 1069 &frame, 1070 &press_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 1071 None, 1072 ); 1073 assert_eq!(s1.captures.top().map(|c| c.id), Some(id("handle"))); 1074 1075 let mut next_frame = HitFrame::new(); 1076 next_frame.push(item( 1077 id("other"), 1078 rect(50.0, 50.0, 20.0, 20.0), 1079 Sense::HOVER, 1080 )); 1081 let s2 = resolve(&s1, &next_frame, &snap_at(pos(55.0, 55.0), 16), None); 1082 assert_eq!(s2.hovered, Some(id("other"))); 1083 } 1084 1085 #[test] 1086 fn release_without_press_clears_stale_capture() { 1087 let mut frame = HitFrame::new(); 1088 frame.push(item( 1089 id("handle"), 1090 rect(0.0, 0.0, 20.0, 20.0), 1091 Sense::DRAGGABLE, 1092 )); 1093 let mut prev = HitState::new(); 1094 prev.captures.push(super::PointerCapture { 1095 id: id("handle"), 1096 button: PointerButton::Primary, 1097 }); 1098 let s = resolve( 1099 &prev, 1100 &frame, 1101 &release_snap(pos(5.0, 5.0), 0, PointerButton::Primary), 1102 None, 1103 ); 1104 assert!(s.captures.is_empty()); 1105 } 1106}