use crate::prelude::*; use std::{cell::Cell, rc::Rc, time::Instant}; struct SenseRsc { ui: Ui, events: EventManager, } impl UiRsc for SenseRsc { fn ui(&self) -> &Ui { &self.ui } fn ui_mut(&mut self) -> &mut Ui { &mut self.ui } fn on_draw(&mut self, active: &ActiveData) { self.events.draw(active); } fn on_undraw(&mut self, active: &ActiveData) { self.events.undraw(active); } fn on_remove(&mut self, id: WidgetId) { self.events.remove(id); } } impl HasState for SenseRsc { type State = (); } #[derive(Clone)] struct RenderProbe; impl Event for RenderProbe {} #[test] fn an_event_can_read_the_completed_frame_while_mutating_ui_data() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; rsc.ui.resize((100.0, 100.0)); let widget = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let weak = widget.weak(); let saw_active = Rc::new(Cell::new(false)); rsc.register_event(weak, RenderProbe, { let saw_active = saw_active.clone(); move |_, rsc| { let render_handle = rsc.ui().render_state(); let render_state = render_handle.get(); saw_active.set(render_state.active.contains_key(&weak.id())); rsc.widgets_mut().set_label(weak, "changed".to_string()); assert!(render_state.active.contains_key(&weak.id())); } }); rsc.draw(&widget.any()); rsc.run_event::(weak, (), &mut ()); assert!(saw_active.get()); assert_eq!(rsc.widgets().label(weak), "changed"); } #[test] #[should_panic(expected = "render state cannot be mutated while it is being read")] fn an_event_cannot_start_a_draw() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let widget = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let weak = widget.weak(); rsc.register_event(weak, RenderProbe, |_, rsc| rsc.draw(None)); rsc.run_event::(weak, (), &mut ()); } impl HasEvents for SenseRsc { fn events(&self) -> &EventManager { &self.events } fn events_mut(&mut self) -> &mut EventManager { &mut self.events } } fn cursor_at(pos: Vec2) -> CursorState { CursorState { pos, exists: true, buttons: Default::default(), scroll_delta: Vec2::ZERO, ..Default::default() } } #[test] fn a_button_over_a_list_scrolls_the_list_and_still_clicks() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; // the case in IRIS_TODO.md's report. let list = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let list_weak = list.weak(); let button = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)); let button_weak = button.weak(); let scrolled = Rc::new(Cell::new(false)); let clicked = Rc::new(Cell::new(false)); { let scrolled = scrolled.clone(); rsc.register_event( list_weak, CursorSense::Scroll(Axis::Y), move |_ctx, _rsc| { scrolled.set(true); }, ); } { let clicked = clicked.clone(); rsc.register_event(button_weak, CursorSense::click(), move |_ctx, _rsc| { clicked.set(true); }); } let root = rsc .ui .widgets .add_strong(Stack { children: vec![list.any(), button.any()], size: StackSize::default(), }) .any(); let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&root, &mut rsc); let mut state = (); let mut scroll_cursor = cursor_at((50.0, 50.0).into()); scroll_cursor.scroll_delta = (0.0, 10.0).into(); render.run_sensors(&mut rsc, &mut state, scroll_cursor, (100.0, 100.0).into()); render.update(&root, &mut rsc); assert!( scrolled.get(), "a scroll over the button must still reach the list underneath it" ); assert!( !clicked.get(), "a scroll is not a click; the button must not have fired" ); let mut click_cursor = cursor_at((50.0, 50.0).into()); click_cursor.buttons.left = ActivationState::Start; render.run_sensors(&mut rsc, &mut state, click_cursor, (100.0, 100.0).into()); render.update(&root, &mut rsc); assert!( clicked.get(), "the button on top must still receive an actual click" ); } #[test] fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let draggable = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any(); let draggable_weak = draggable.weak(); let dropped = Rc::new(Cell::new(false)); { let dropped = dropped.clone(); rsc.register_event( draggable_weak, CursorSense::click_or_drag() | CursorSense::unclick() | CursorSense::Drop, move |ctx, rsc| match ctx.data.sense { CursorSense::PressStart(_) | CursorSense::Pressing(_) => { ctx.data.pointer.capture(draggable_weak.id()); let _ = rsc; } CursorSense::Drop => dropped.set(true), _ => {} }, ); } let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&draggable, &mut rsc); let mut state = (); let mut press = cursor_at((5.0, 5.0).into()); press.buttons.left = ActivationState::Start; render.run_sensors(&mut rsc, &mut state, press, (100.0, 100.0).into()); render.update(&draggable, &mut rsc); assert_eq!( pointer_input(&mut rsc).holder(), Some(draggable.id()), "the press should have taken capture" ); let mut release = cursor_at((95.0, 95.0).into()); release.buttons.left = ActivationState::End; render.run_sensors(&mut rsc, &mut state, release, (100.0, 100.0).into()); render.update(&draggable, &mut rsc); assert!( dropped.get(), "a release outside every widget's hit region must still reach \ the widget holding pointer capture" ); assert_eq!( pointer_input(&mut rsc).holder(), None, "Drop must release the capture" ); } #[test] fn capturing_one_widget_starves_every_other_widget_of_events() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let a = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let a_weak = a.weak(); let b = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)); let b_weak = b.weak(); let b_hovered = Rc::new(Cell::new(false)); { let b_hovered = b_hovered.clone(); rsc.register_event(b_weak, CursorSense::Hovering, move |_ctx, _rsc| { b_hovered.set(true); }); } let root = rsc .ui .widgets .add_strong(Stack { children: vec![a.any(), b.any()], size: StackSize::default(), }) .any(); let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&root, &mut rsc); pointer_input(&mut rsc).set_holder(Some(a_weak.id())); let mut state = (); let cursor = cursor_at((50.0, 50.0).into()); render.run_sensors(&mut rsc, &mut state, cursor, (100.0, 100.0).into()); render.update(&root, &mut rsc); assert!( !b_hovered.get(), "while a's drag holds capture, b must see no hover at all" ); } #[test] fn a_finger_drag_over_a_scroll_area_pans_it() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let scroll_strong = rect(PaintId::WHITE) .height(Len::abs(1000.0)) .scrollable(Axis::Y, Pin::Start) .add_strong(&mut rsc); let scroll = scroll_strong.weak(); let root = scroll_strong.any(); let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&root, &mut rsc); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0); render.update(&root, &mut rsc); assert_eq!(rsc.ui.widgets.get(&scroll).unwrap().amt(), 0.0); let mut state = (); let mut down = cursor_at((50.0, 80.0).into()); down.buttons.left = ActivationState::Start; render.run_sensors(&mut rsc, &mut state, down, (100.0, 100.0).into()); render.update(&root, &mut rsc); assert_eq!( rsc.ui.widgets.get(&scroll).unwrap().amt(), 0.0, "the touch-down alone must not move anything" ); let mut nudge = cursor_at((50.0, 80.0 - (DRAG_SLOP - 1.0)).into()); nudge.buttons.left = ActivationState::On; render.run_sensors(&mut rsc, &mut state, nudge, (100.0, 100.0).into()); render.update(&root, &mut rsc); assert_eq!( rsc.ui.widgets.get(&scroll).unwrap().amt(), 0.0, "a press inside DRAG_SLOP must not scroll" ); let mut drag = cursor_at((50.0, 80.0 - (DRAG_SLOP + 40.0)).into()); drag.buttons.left = ActivationState::On; render.run_sensors(&mut rsc, &mut state, drag, (100.0, 100.0).into()); render.update(&root, &mut rsc); let after = rsc.ui.widgets.get(&scroll).unwrap().amt(); assert!( (after - 40.0).abs() < 0.01, "expected the 40px past the slop to pan it, got {after}" ); assert_eq!(pointer_input(&mut rsc).holder(), Some(scroll.id())); } #[test] fn the_first_events_batched_samples_are_dated_apart() { const MS: i64 = 1_000_000; let now = Instant::now(); let clock = DeviceClock::anchored(now, 12 * MS, 0); assert_eq!( clock.at(12 * MS), now, "the event's own sample is the one that arrived now" ); let batch = [clock.at(0), clock.at(4 * MS), clock.at(8 * MS)]; assert!( batch[0] < batch[1] && batch[1] < batch[2] && batch[2] < now, "the batch must keep the 4ms between its samples, got {:?}", batch .iter() .map(|t| now.duration_since(*t)) .collect::>() ); assert_eq!(clock.ms_since_anchor(8 * MS), 8); } #[test] fn the_clock_orders_samples_across_events() { const MS: i64 = 1_000_000; let mut clock = DeviceClock::anchored(Instant::now(), 12 * MS, 0); let first = clock.sample(12 * MS); let second = clock.sample(28 * MS); assert!(second > first); assert_eq!( second.duration_since(first), std::time::Duration::from_millis(16) ); } #[test] fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let scroll_strong = rect(PaintId::WHITE) .height(Len::abs(1000.0)) .scrollable(Axis::Y, Pin::Start) .add_strong(&mut rsc); let scroll = scroll_strong.weak(); let root = scroll_strong.any(); let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&root, &mut rsc); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0); render.update(&root, &mut rsc); let mut state = (); let win = Vec2::new(100.0, 100.0); let mut send = |render: &mut UiRenderState, rsc: &mut SenseRsc, y: f32, button| { let mut c = cursor_at((50.0, y).into()); c.buttons.left = button; render.run_sensors(rsc, &mut state, c, win); render.update(&root, rsc); }; send(&mut render, &mut rsc, 80.0, ActivationState::Start); send( &mut render, &mut rsc, 80.0 - (DRAG_SLOP + 40.0), ActivationState::On, ); let after_first = rsc.ui.widgets.get(&scroll).unwrap().amt(); assert!((after_first - 40.0).abs() < 0.01, "amt={after_first}"); send(&mut render, &mut rsc, 400.0, ActivationState::End); assert_eq!( pointer_input(&mut rsc).holder(), None, "the release must give the pointer back" ); send(&mut render, &mut rsc, 80.0, ActivationState::Start); let after_second = rsc.ui.widgets.get(&scroll).unwrap().amt(); assert!( (after_second - after_first).abs() < 0.01, "a fresh touch-down moved the content by {} -- the previous \ gesture was never closed", after_second - after_first, ); } #[test] fn taking_the_pointer_cancels_everyone_else_tracking_the_press() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let capturer = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let capturer_weak = capturer.weak(); let bystander = rsc.ui.widgets.add_strong(Stack { children: vec![capturer.any()], size: StackSize::default(), }); let bystander_weak = bystander.weak(); let capturer_saw = Rc::new(Cell::new(0u32)); { let capturer_saw = capturer_saw.clone(); rsc.register_event( capturer_weak, CursorSense::drag(Axis::X), move |ctx, _rsc| { capturer_saw.set(capturer_saw.get() + 1); if matches!(ctx.data.sense, CursorSense::Pressing(_)) { ctx.data.pointer.capture(capturer_weak.id()); } }, ); } let cancelled = Rc::new(Cell::new(0u32)); let ended = Rc::new(Cell::new(0u32)); { let (cancelled, ended) = (cancelled.clone(), ended.clone()); rsc.register_event( bystander_weak, CursorSense::drag(Axis::Y), move |ctx, _rsc| match ctx.data.sense { CursorSense::Cancel => cancelled.set(cancelled.get() + 1), CursorSense::PressEnd(_) | CursorSense::Drop => ended.set(ended.get() + 1), _ => {} }, ); } let root = bystander.any(); let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&root, &mut rsc); let mut state = (); let win = Vec2::new(100.0, 100.0); let mut down = cursor_at((50.0, 50.0).into()); down.buttons.left = ActivationState::Start; render.run_sensors(&mut rsc, &mut state, down, win); render.update(&root, &mut rsc); assert_eq!(cancelled.get(), 0, "nothing has captured yet"); let mut moved = cursor_at((80.0, 50.0).into()); moved.buttons.left = ActivationState::On; render.run_sensors(&mut rsc, &mut state, moved, win); render.update(&root, &mut rsc); assert!(capturer_saw.get() > 0, "the capturer never saw the press"); assert_eq!( pointer_input(&mut rsc).holder(), Some(capturer_weak.id()), "the capture should have been taken on this frame" ); assert_eq!( cancelled.get(), 1, "the widget that lost the gesture must be told exactly once" ); let mut more = cursor_at((50.0, 10.0).into()); more.buttons.left = ActivationState::On; render.run_sensors(&mut rsc, &mut state, more, win); render.update(&root, &mut rsc); let mut up = cursor_at((50.0, 10.0).into()); up.buttons.left = ActivationState::End; render.run_sensors(&mut rsc, &mut state, up, win); render.update(&root, &mut rsc); assert_eq!(cancelled.get(), 1, "cancelled more than once"); assert_eq!( ended.get(), 0, "a cancelled widget must not also be told the gesture ended \ normally -- acting on that is the tap it never made" ); } #[test] fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() { for (name, to, pans, still) in [ ("vertical", Vec2::new(50.0, 80.0 - (DRAG_SLOP + 40.0)), 0, 1), ( "horizontal", Vec2::new(50.0 - (DRAG_SLOP + 40.0), 80.0), 1, 0, ), ] { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let seen = Rc::new(Cell::new(None)); let record = seen.clone(); let outer_strong = rect(PaintId::WHITE) .width(Len::abs(1000.0)) .height(Len::abs(1000.0)) .scrollable(Axis::X, Pin::Start) .with_id(move |_rsc, id| { record.set(Some(id)); id }) .layer_offset(1) .scrollable(Axis::Y, Pin::Start) .add_strong(&mut rsc); let inner = seen.get().unwrap(); let outer = outer_strong.weak(); let root = outer_strong.any(); let areas = [outer, inner]; let mut render = UiRenderState::new(); render.resize((100.0, 100.0)); render.update(&root, &mut rsc); for a in areas { rsc.ui.widgets.get_mut(&a).unwrap().scroll(0.0); } render.update(&root, &mut rsc); let mut state = (); let win = Vec2::new(100.0, 100.0); let mut down = cursor_at((50.0, 80.0).into()); down.buttons.left = ActivationState::Start; render.run_sensors(&mut rsc, &mut state, down, win); render.update(&root, &mut rsc); let mut drag = cursor_at(to); drag.buttons.left = ActivationState::On; render.run_sensors(&mut rsc, &mut state, drag, win); render.update(&root, &mut rsc); let moved = rsc.ui.widgets.get(&areas[pans]).unwrap().amt(); let unmoved = rsc.ui.widgets.get(&areas[still]).unwrap().amt(); assert!( (moved - 40.0).abs() < 0.01, "a {name} drag should have panned the {name} area by the 40px \ past the slop, got {moved}" ); assert_eq!( unmoved, 0.0, "a {name} drag must not move the area that owns the other axis" ); } } #[test] fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() { let mut rsc = SenseRsc { ui: Ui::default(), events: EventManager::default(), }; let seen: [Rc>>>; 2] = Default::default(); let half = |slot: &Rc>>>| { let record = slot.clone(); rect(PaintId::WHITE) .height(Len::abs(1000.0)) .scrollable(Axis::Y, Pin::Start) .with_id(move |_rsc, id| { record.set(Some(id)); id }) .height(Len::rel(0.5)) }; let root = (half(&seen[0]), half(&seen[1])) .span(Dir::DOWN) .add_strong(&mut rsc) .any(); let (top_w, bottom_w) = (seen[0].get().unwrap(), seen[1].get().unwrap()); let win: Vec2 = (100.0, 200.0).into(); let mut render = UiRenderState::new(); render.resize((win.x, win.y)); render.update(&root, &mut rsc); for w in [&top_w, &bottom_w] { rsc.ui.widgets.get_mut(w).unwrap().scroll(0.0); } render.update(&root, &mut rsc); let mut state = (); let base = Instant::now(); let mut t = 0; let sample = |render: &mut UiRenderState, rsc: &mut SenseRsc, state: &mut (), y: f32, button: ActivationState, at_ms: u64| { let mut c = cursor_at((50.0, y).into()); c.buttons.left = button; c.time = base + std::time::Duration::from_millis(at_ms); render.run_sensors(rsc, state, c, win); render.update(&root, rsc); }; sample( &mut render, &mut rsc, &mut state, 50.0, ActivationState::Start, t, ); for y in [44.0, 32.0, 14.0] { t += 8; sample(&mut render, &mut rsc, &mut state, y, ActivationState::On, t); } t += 8; sample( &mut render, &mut rsc, &mut state, 14.0, ActivationState::End, t, ); assert!( rsc.ui.widgets.get(&top_w).unwrap().is_scrolling(), "the flick must leave the top area coasting -- the press below is \ only dangerous while something is still moving", ); let flung_to = rsc.ui.widgets.get(&top_w).unwrap().amt(); t += 8; sample( &mut render, &mut rsc, &mut state, 150.0, ActivationState::Start, t, ); t += 8; sample( &mut render, &mut rsc, &mut state, 150.0 - (DRAG_SLOP + 40.0), ActivationState::On, t, ); let moved = rsc.ui.widgets.get(&bottom_w).unwrap().amt(); assert!( (moved - 40.0).abs() < 0.01, "the area actually under the finger should have panned by the 40px \ past the slop, got {moved}" ); assert_eq!( rsc.ui.widgets.get(&top_w).unwrap().amt(), flung_to, "the area the pointer had left must not have seen the press at all -- \ a catch would have stopped its fling on the touch-down" ); assert_eq!( pointer_input(&mut rsc).holder(), Some(bottom_w.id()), "the gesture belongs to the widget under the finger", ); }