use super::*; use iced::advanced::widget::operation::{self, Focusable, Scrollable}; use iced::widget::{Space, button, column, container, mouse_area, scrollable, text_input}; use iced::{Point, Theme, event, keyboard}; type View = Element<'static, Message, Theme, ()>; const BOUNDS: Rectangle = Rectangle { x: 0., y: 0., width: 400., height: 300., }; #[derive(Clone, Debug, PartialEq, Eq)] enum Message { Primary(u8), Related(u8), BackgroundButton, BackgroundRelease, Submit(u8), Edit, } fn rows(version: u8) -> View { column![ button(Space::new()) .width(200.) .height(40.) .padding(0) .on_press(Message::Primary(version)), button(Space::new()) .width(200.) .height(40.) .padding(0) .on_press(Message::Related(version)), ] .into() } fn guarded(generation: Option, version: u8) -> View { let child: View = if generation.is_some() { rows(version) } else { Space::new().into() }; Guard::new( generation.map(|publication| (publication, Id::nil())), child, ) .into() } fn press() -> Event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) } fn release() -> Event { Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) } fn enter() -> Event { Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Enter), modified_key: keyboard::Key::Named(keyboard::key::Named::Enter), physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Enter), location: keyboard::Location::Standard, modifiers: keyboard::Modifiers::empty(), text: None, repeat: false, }) } fn touch_down(id: u64, position: Point) -> Event { Event::Touch(touch::Event::FingerPressed { id: touch::Finger(id), position, }) } fn touch_up(id: u64, position: Point) -> Event { Event::Touch(touch::Event::FingerLifted { id: touch::Finger(id), position, }) } fn touch_lost(id: u64, position: Point) -> Event { Event::Touch(touch::Event::FingerLost { id: touch::Finger(id), position, }) } struct Mounted { view: View, tree: widget::Tree, node: layout::Node, } impl Mounted { fn new(mut view: View) -> Self { let mut tree = widget::Tree::new(view.as_widget()); let node = view.as_widget_mut().layout( &mut tree, &(), &layout::Limits::new(Size::ZERO, BOUNDS.size()), ); Self { view, tree, node } } fn replace(&mut self, mut view: View) { self.tree.diff(view.as_widget()); self.node = view.as_widget_mut().layout( &mut self.tree, &(), &layout::Limits::new(Size::ZERO, BOUNDS.size()), ); self.view = view; } fn event(&mut self, event: Event, point: Point) -> (Vec, event::Status) { self.event_with_capture(event, point, false) } fn event_with_capture( &mut self, event: Event, point: Point, captured: bool, ) -> (Vec, event::Status) { let mut messages = Vec::new(); let mut shell = Shell::new(&mut messages); if captured { shell.capture_event(); } self.view.as_widget_mut().update( &mut self.tree, &event, Layout::new(&self.node), mouse::Cursor::Available(point), &(), &mut iced::advanced::clipboard::Null, &mut shell, &BOUNDS, ); let status = shell.event_status(); (messages, status) } fn operate(&mut self, operation: &mut dyn widget::Operation) { self.view .as_widget_mut() .operate(&mut self.tree, Layout::new(&self.node), &(), operation); } fn focus(&mut self) { self.operate(&mut operation::focusable::focus::<()>("row".into())); } fn focused(&mut self) -> bool { #[derive(Default)] struct Focus(bool); impl widget::Operation for Focus { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { visit(self); } fn focusable( &mut self, _: Option<&widget::Id>, _: Rectangle, state: &mut dyn Focusable, ) { self.0 |= state.is_focused(); } } let mut focus = Focus::default(); self.operate(&mut focus); focus.0 } fn scroll(&mut self) -> Vector { #[derive(Default)] struct Offset(Option); impl widget::Operation for Offset { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { visit(self); } fn scrollable( &mut self, _: Option<&widget::Id>, _: Rectangle, _: Rectangle, translation: Vector, _: &mut dyn Scrollable, ) { self.0 = Some(translation); } } let mut offset = Offset::default(); self.operate(&mut offset); offset.0.unwrap() } } #[test] fn current_primary_and_related_mouse_clicks_survive_same_publication_diff() { for (y, expected) in [(20., Message::Primary(1)), (60., Message::Related(1))] { let generation = Id::new_v4(); let point = Point::new(20., y); let mut ui = Mounted::new(guarded(Some(generation), 1)); assert_eq!(ui.event(press(), point), (vec![], event::Status::Captured)); ui.replace(guarded(Some(generation), 1)); assert_eq!( ui.event(release(), point), (vec![expected], event::Status::Captured) ); } } #[test] fn same_publication_new_visit_cancels_armed_rows_without_a_hidden_tree_frame() { for touch in [false, true] { for (y, expected) in [(20., Message::Primary(2)), (60., Message::Related(2))] { let publication = Id::new_v4(); let old_visit = Id::new_v4(); let new_visit = Id::new_v4(); let point = Point::new(20., y); let mut ui = Mounted::new(Guard::new(Some((publication, old_visit)), rows(1)).into()); let down = || if touch { touch_down(7, point) } else { press() }; let up = || if touch { touch_up(7, point) } else { release() }; assert_eq!(ui.event(down(), point), (vec![], event::Status::Captured)); // Hide→Show has already happened in application state. No None // publication or empty child is ever diffed into this retained Tree. ui.replace(Guard::new(Some((publication, new_visit)), rows(2)).into()); assert_eq!(ui.event(up(), point), (vec![], event::Status::Captured)); assert_eq!(ui.event(down(), point), (vec![], event::Status::Captured)); assert_eq!( ui.event(up(), point), (vec![expected], event::Status::Captured) ); } } } #[test] fn negative_control_iced_button_retargets_a_press_after_plain_tree_diff() { for (y, expected) in [(20., Message::Primary(2)), (60., Message::Related(2))] { let point = Point::new(20., y); let mut ui = Mounted::new(rows(1)); assert!(ui.event(press(), point).0.is_empty()); ui.replace(rows(2)); assert_eq!(ui.event(release(), point).0, vec![expected]); } } #[test] fn replaced_primary_and_related_rows_consume_stale_release_then_accept_new_click() { for y in [20., 60.] { let point = Point::new(20., y); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); ui.event(press(), point); ui.replace(guarded(Some(Id::new_v4()), 2)); ui.replace(guarded(Some(Id::new_v4()), 3)); assert_eq!( ui.event(release(), point), (vec![], event::Status::Captured) ); ui.event(press(), point); let expected = if y < 40. { Message::Primary(3) } else { Message::Related(3) }; assert_eq!( ui.event(release(), point), (vec![expected], event::Status::Captured) ); } } #[test] fn hidden_zero_size_list_consumes_release_outside_old_bounds_once() { let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); ui.event(press(), Point::new(20., 20.)); ui.replace(guarded(None, 0)); ui.replace(guarded(None, 0)); assert_eq!(ui.node.size(), Size::ZERO); let outside = Point::new(350., 250.); assert_eq!( ui.event(release(), outside), (vec![], event::Status::Captured) ); assert_eq!( ui.event(release(), outside), (vec![], event::Status::Ignored) ); ui.replace(guarded(Some(Id::new_v4()), 2)); assert_eq!(ui.event(release(), Point::new(20., 20.)).0, vec![]); } fn with_background(content: View) -> View { mouse_area( button(container(content).width(250.).height(100.)) .padding(0) .on_press(Message::BackgroundButton), ) .on_release(Message::BackgroundRelease) .into() } #[test] fn stale_release_does_not_bubble_to_real_background_controls() { let point = Point::new(20., 20.); let mut ui = Mounted::new(with_background(guarded(Some(Id::new_v4()), 1))); ui.event(press(), point); ui.replace(with_background(guarded(None, 0))); assert_eq!( ui.event(release(), point), (vec![], event::Status::Captured) ); // The enclosing real button still supports a fresh ordinary click. ui.event(press(), point); assert_eq!( ui.event(release(), point).0, vec![Message::BackgroundButton] ); } #[test] fn negative_control_removing_unguarded_rows_exposes_background_release() { let point = Point::new(20., 20.); let mut ui = Mounted::new(with_background(rows(1))); ui.event(press(), point); ui.replace(with_background(Space::new().into())); assert_eq!( ui.event(release(), point).0, vec![Message::BackgroundRelease] ); } #[test] fn current_touch_activates_primary_and_related_and_cancel_does_not_activate() { for (y, expected) in [(20., Message::Primary(1)), (60., Message::Related(1))] { let point = Point::new(20., y); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); assert_eq!( ui.event(touch_down(1, point), point), (vec![], event::Status::Captured) ); assert_eq!( ui.event(touch_up(1, point), point), (vec![expected], event::Status::Captured) ); ui.event(touch_down(2, point), point); assert_eq!( ui.event(touch_lost(2, point), point), (vec![], event::Status::Captured) ); assert!(ui.event(touch_up(2, point), point).0.is_empty()); } } #[test] fn replaced_touch_consumes_lift_or_cancel_even_when_hidden() { for hide in [false, true] { for lost in [false, true] { for y in [20., 60.] { let point = Point::new(20., y); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); ui.event(touch_down(1, point), point); ui.replace(guarded(if hide { None } else { Some(Id::new_v4()) }, 2)); let end = if lost { touch_lost(1, point) } else { touch_up(1, point) }; assert_eq!(ui.event(end, point), (vec![], event::Status::Captured)); } } } } #[test] fn unrelated_finger_or_mouse_release_cannot_complete_another_fingers_press() { let point = Point::new(20., 20.); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); ui.event(touch_down(1, point), point); ui.event(touch_down(2, point), point); assert_eq!( ui.event(touch_up(2, point), point), (vec![], event::Status::Captured) ); assert_eq!( ui.event(release(), point), (vec![], event::Status::Captured) ); assert_eq!( ui.event(touch_up(1, point), point).0, vec![Message::Primary(1)] ); } fn keyboard_row(version: u8) -> View { // Iced 0.14.2 Button has no focus/keyboard activation. A real focusable // submit control tests the wrapper's focus lifecycle without inventing it. text_input("Diagnostic action", "row") .id("row") .on_input(|_| Message::Edit) .on_submit(Message::Submit(version)) .into() } #[test] fn same_publication_preserves_focus_but_replacement_never_inherits_activation() { let generation = Id::new_v4(); let mut ui = Mounted::new(Guard::new(Some((generation, Id::nil())), keyboard_row(1)).into()); ui.focus(); assert!(ui.focused()); ui.replace(Guard::new(Some((generation, Id::nil())), keyboard_row(1)).into()); assert!(ui.focused()); assert_eq!(ui.event(enter(), Point::ORIGIN).0, vec![Message::Submit(1)]); ui.replace(Guard::new(Some((Id::new_v4(), Id::nil())), keyboard_row(2)).into()); assert!(!ui.focused()); assert!(ui.event(enter(), Point::ORIGIN).0.is_empty()); ui.focus(); assert_eq!(ui.event(enter(), Point::ORIGIN).0, vec![Message::Submit(2)]); ui.replace(Guard::new(None, Space::new()).into()); ui.replace(Guard::new(Some((Id::new_v4(), Id::nil())), keyboard_row(3)).into()); assert!(!ui.focused()); assert!(ui.event(enter(), Point::ORIGIN).0.is_empty()); } #[test] fn negative_control_plain_focusable_row_migrates_activation_to_replacement() { let mut ui = Mounted::new(keyboard_row(1)); ui.focus(); ui.replace(keyboard_row(2)); assert!(ui.focused()); assert_eq!(ui.event(enter(), Point::ORIGIN).0, vec![Message::Submit(2)]); } #[test] fn outer_scroll_offset_survives_same_and_changed_publications() { fn pane(generation: Id) -> View { scrollable(Guard::new( Some((generation, Id::nil())), column![rows(1), Space::new().height(600.)], )) .width(240.) .height(100.) .into() } let generation = Id::new_v4(); let mut ui = Mounted::new(pane(generation)); ui.event( Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Pixels { x: 0., y: -120. }, }), Point::new(20., 20.), ); let offset = ui.scroll(); assert_eq!(offset, Vector::new(0., 120.)); ui.replace(pane(generation)); assert_eq!(ui.scroll(), offset); ui.replace(pane(Id::new_v4())); assert_eq!(ui.scroll(), offset); } #[test] fn stale_touch_release_does_not_disarm_a_new_current_gesture() { let point = Point::new(20., 20.); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); ui.event(touch_down(1, point), point); ui.replace(guarded(Some(Id::new_v4()), 2)); ui.event(touch_down(2, point), point); assert_eq!( ui.event(touch_up(1, point), point), (vec![], event::Status::Captured) ); assert_eq!( ui.event(touch_up(2, point), point).0, vec![Message::Primary(2)] ); } #[test] fn fresh_mouse_press_after_missing_stale_release_can_activate_current_row() { let point = Point::new(20., 20.); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); ui.event(press(), point); ui.replace(guarded(Some(Id::new_v4()), 2)); // The platform delivers another down without the previous up. ui.event(press(), point); assert_eq!(ui.event(release(), point).0, vec![Message::Primary(2)]); } #[test] fn missed_press_is_ignored_and_dragging_release_outside_does_not_activate() { let inside = Point::new(20., 20.); let outside = Point::new(350., 250.); let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); assert_eq!(ui.event(press(), outside), (vec![], event::Status::Ignored)); assert_eq!( ui.event(release(), inside), (vec![], event::Status::Ignored) ); ui.event(press(), inside); assert_eq!( ui.event(release(), outside), (vec![], event::Status::Captured) ); assert_eq!( ui.event(release(), inside), (vec![], event::Status::Ignored) ); } #[test] fn same_publication_missing_terminal_recovers_on_fresh_mouse_or_touch_down() { let primary = Point::new(20., 20.); let related = Point::new(20., 60.); for is_touch in [false, true] { let generation = Id::new_v4(); let mut ui = Mounted::new(guarded(Some(generation), 1)); let down = |point| { if is_touch { touch_down(1, point) } else { press() } }; let up = |point| { if is_touch { touch_up(1, point) } else { release() } }; ui.event(down(primary), primary); ui.replace(guarded(Some(generation), 1)); // The first gesture never receives its terminal event; same Id means // no publication reset can accidentally make this regression pass. assert_eq!( ui.event(down(related), related), (vec![], event::Status::Captured) ); assert_eq!(ui.event(up(related), related).0, vec![Message::Related(1)]); assert!(ui.event(up(primary), primary).0.is_empty()); } } #[test] fn already_captured_terminal_cancels_child_without_activation_or_lingering_press() { let primary = Point::new(20., 20.); let related = Point::new(20., 60.); for is_touch in [false, true] { let mut ui = Mounted::new(guarded(Some(Id::new_v4()), 1)); let down = |point| { if is_touch { touch_down(1, point) } else { press() } }; let up = |point| { if is_touch { touch_up(1, point) } else { release() } }; ui.event(down(primary), primary); assert_eq!( ui.event_with_capture(up(primary), primary, true), (vec![], event::Status::Captured) ); // An unpaired second release must not expose a lingering button press. assert_eq!( ui.event(up(primary), primary), (vec![], event::Status::Ignored) ); ui.event(down(related), related); assert_eq!(ui.event(up(related), related).0, vec![Message::Related(1)]); } } #[test] fn captured_terminal_cancellation_preserves_same_publication_focusable_child_state() { let generation = Id::new_v4(); let mut ui = Mounted::new( Guard::new( Some((generation, Id::nil())), column![rows(1), keyboard_row(1)], ) .into(), ); ui.event(press(), Point::new(20., 20.)); // Focus through an operation avoids making the old pointer gesture whole. ui.focus(); assert!(ui.focused()); assert!( ui.event_with_capture(release(), Point::new(20., 20.), true) .0 .is_empty() ); assert!( ui.focused(), "gesture cancellation must not remount the row tree" ); assert_eq!(ui.event(enter(), Point::ORIGIN).0, vec![Message::Submit(1)]); } #[test] fn hidden_zero_height_scroller_delivers_cancelled_mouse_and_touch_terminals_to_guard() { fn pane(generation: Option) -> View { scrollable(guarded(generation, 1)) .width(240.) .height(if generation.is_some() { 100. } else { 0. }) .into() } let point = Point::new(20., 20.); let outside = Point::new(350., 250.); for is_touch in [false, true] { let mut ui = Mounted::new(pane(Some(Id::new_v4()))); let down = if is_touch { touch_down(1, point) } else { press() }; assert_eq!(ui.event(down, point), (vec![], event::Status::Captured)); ui.replace(pane(None)); ui.replace(pane(None)); assert_eq!(ui.node.size().height, 0.); let up = if is_touch { touch_up(1, outside) } else { release() }; assert_eq!(ui.event(up, outside), (vec![], event::Status::Captured)); } }