use super::*; use crate::clipboard::NoClipboard; use crate::event::PointerInput; use crate::text::TextContext; use guiduck_scene::geom::Point; use taffy::prelude::length; const VIEWPORT: Size = Size::new(200.0, 200.0); fn sample_font() -> guiduck_scene::paint::Blob { static FONT: &[u8] = include_bytes!("../../../../guiduck-samples/fonts/DejaVuSans.ttf"); guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT)) } /// Two 100×50 boxes stacked; only the first has a tooltip. fn fixture() -> (WidgetTree, WidgetId, WidgetId) { let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()])); let root = tree.insert( super::super::Container::new(), taffy::Style { flex_direction: taffy::FlexDirection::Column, ..Default::default() }, None, ); let boxes: Vec = (0..2) .map(|_| { tree.insert( super::super::Container::new(), taffy::Style { size: taffy::Size { width: length(100.0_f32), height: length(50.0_f32), }, ..Default::default() }, Some(root), ) }) .collect(); tree.set_tooltip(boxes[0], Some("the first one".to_owned())); tree.render_frame(VIEWPORT); (tree, boxes[0], boxes[1]) } fn hover(tree: &mut WidgetTree, at: Point) { tree.dispatch_pointer(PointerInput::Moved(at), &mut NoClipboard); tree.render_frame(VIEWPORT); } fn showing(tree: &WidgetTree) -> bool { tree.overlay_roots().iter().any(|root| { tree.children(*root) .iter() .any(|c| tree.widget::(*c).is_some()) }) } #[test] fn a_tooltip_waits_for_the_pointer_to_rest() { let (mut tree, _with, _without) = fixture(); let now = std::time::Instant::now(); hover(&mut tree, Point::new(50.0, 25.0)); assert!(!showing(&tree), "not the instant the pointer arrives"); // Resting arms a deadline — the tree says "in this long" and the platform // says when now is, the same no-hidden-clock rule widgets follow. let due = tree.next_wake(now).expect("a tooltip is pending"); assert_eq!(due, now + TOOLTIP_DELAY); tree.tick(now + TOOLTIP_DELAY); tree.render_frame(VIEWPORT); assert!(showing(&tree), "it appears once the pointer has rested"); } #[test] fn moving_away_takes_it_back() { let (mut tree, _with, _without) = fixture(); let now = std::time::Instant::now(); hover(&mut tree, Point::new(50.0, 25.0)); tree.next_wake(now); tree.tick(now + TOOLTIP_DELAY); tree.render_frame(VIEWPORT); assert!(showing(&tree)); // A tooltip is about the thing you are pointing at; point elsewhere and // it is about nothing. hover(&mut tree, Point::new(50.0, 75.0)); assert!(!showing(&tree)); assert!( tree.next_wake(now).is_none(), "and nothing is pending for a widget with no tooltip" ); } #[test] fn a_widget_without_one_arms_nothing() { // The idle guarantee: hovering plain content must not start a clock. let (mut tree, _with, _without) = fixture(); hover(&mut tree, Point::new(50.0, 75.0)); assert!(tree.next_wake(std::time::Instant::now()).is_none()); } #[test] fn an_open_tooltip_does_not_re_arm_itself() { // Once it is showing there is nothing to wait for; a deadline that kept // renewing would be a frame every 600ms, forever. let (mut tree, _with, _without) = fixture(); let now = std::time::Instant::now(); hover(&mut tree, Point::new(50.0, 25.0)); tree.next_wake(now); tree.tick(now + TOOLTIP_DELAY); tree.render_frame(VIEWPORT); assert!(showing(&tree)); assert!(tree.next_wake(now + TOOLTIP_DELAY).is_none()); } #[test] fn the_pointer_passes_through_a_tooltip() { // It sits *below* its widget, which is exactly where the next widget is. // If it took hover it would steal it from the thing it describes — and // since losing that hover is what dismisses it, it would flicker itself // out of existence. So the pointer goes straight through. let (mut tree, with, without) = fixture(); let now = std::time::Instant::now(); hover(&mut tree, Point::new(50.0, 25.0)); tree.next_wake(now); tree.tick(now + TOOLTIP_DELAY); tree.render_frame(VIEWPORT); assert!(showing(&tree)); // The tooltip is anchored below `with`, over `without`'s territory — and // hit testing finds `without`, not the tooltip. assert_eq!(tree.hit_test(Point::new(50.0, 60.0)), Some(without)); assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(with)); }