Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map/tests/empty_pane_gestures.rs

//! **An empty map pane is still a control** (stinky `seam-17`).
//!
//! `MapView::view` allocates its canvas with `Sense::click_and_drag()` and then — in
//! exactly the two states where a user most wants to move the camera, nothing loaded
//! and a degenerate envelope — returned before it read a single gesture. A drag went
//! nowhere. A wheel notch went nowhere. The only way out of an empty view was a control
//! outside the pane.
//!
//! It is the same defect `MapFacet::ui` records against itself one widget over ("korp's
//! call site says 'persist the camera the user drove' — the user could not drive it"),
//! and it survived because `Sense::click_and_drag()` is the part everyone checks.
//!
//! # What is asserted
//!
//! The **messages the widget returns**, which is its applied output: `view` is an ELM
//! view, it mutates nothing, and `Vec<Msg>` is the entire effect it has on the world.
//! A `resp.dragged()` assertion would be a state round-trip on egui, not on this code.
//!
//! The pane is driven through a real `egui::Context` with real `RawInput` — a press,
//! then a move — so the drag is egui's own, not a simulated flag.
//!
//! Run: `cargo test -p facett-map --test empty_pane_gestures`

use egui::{Modifiers, Pos2, RawInput, Rect, Vec2, pos2, vec2};
use facett_map::{MapView, Msg};

/// Drive `MapView::view` across several frames and hand back the LAST frame's messages.
///
/// egui resolves an interaction at the START of a frame against the rects the PREVIOUS
/// frame registered, so a gesture needs a warm-up: one frame to lay the widget out, one
/// to put the pointer on it, one to press, one to move. Fewer frames and the widget has
/// no interaction to report — which is what the control test in this file exists to
/// catch, and did on the first draft.
fn drive(map: &MapView, frames: Vec<Vec<egui::Event>>) -> Vec<Msg> {
    let ctx = egui::Context::default();
    let mut last = Vec::new();
    // Warm-up: lay the widget out so the next frame has a rect to hit-test against.
    for _ in 0..2 {
        let input = RawInput {
            screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))),
            ..Default::default()
        };
        ctx.run_ui(input, |ui| {
            let _ = map.view(ui);
        }).textures_delta.clear();
    }
    for events in frames {
        let input = RawInput {
            screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))),
            events,
            ..Default::default()
        };
        let mut msgs = Vec::new();
        ctx.run_ui(input, |ui| {
            msgs = map.view(ui);
        }).textures_delta.clear();
        last = msgs;
    }
    last
}

/// Somewhere inside the pane. The widget is laid out under a header row, so this sits
/// well down the available space.
const OVER_PANE: Pos2 = pos2(400.0, 400.0);

#[test]
fn an_empty_pane_still_pans_and_zooms() {
    // No points, no ways — the `empty` arm, which paints "no geo data" and used to
    // return before reading any gesture.
    let map = MapView::new("empty", Vec::new());

    // ── DRAG: press, then move. ───────────────────────────────────────────────
    let drag = drive(
        &map,
        vec![
            vec![
                egui::Event::PointerMoved(OVER_PANE),
                egui::Event::PointerButton {
                    pos: OVER_PANE,
                    button: egui::PointerButton::Primary,
                    pressed: true,
                    modifiers: Modifiers::default(),
                },
            ],
            vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(40.0, -25.0))],
            vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(70.0, -40.0))],
        ],
    );
    let panned: Vec<[f32; 2]> = drag
        .iter()
        .filter_map(|m| if let Msg::PanBy(d) = m { Some(*d) } else { None })
        .collect();
    assert!(
        !panned.is_empty(),
        "a drag over an EMPTY pane must return a PanBy — it returned {drag:?}. The pane senses \
         click_and_drag and then dropped the gesture, so the only way out of an empty view was a \
         control outside the map."
    );
    assert!(
        panned.iter().any(|d| d[0] != 0.0 || d[1] != 0.0),
        "and the delta must be the real one, not a zero: {panned:?}"
    );

    // ── WHEEL ────────────────────────────────────────────────────────────────
    let wheel = drive(
        &map,
        vec![
            vec![egui::Event::PointerMoved(OVER_PANE)],
            vec![
                egui::Event::PointerMoved(OVER_PANE),
                egui::Event::MouseWheel {
                    unit: egui::MouseWheelUnit::Point,
                    delta: vec2(0.0, 50.0),
                    modifiers: Modifiers::default(),
                    phase: egui::TouchPhase::Move,
                },
            ],
        ],
    );
    let zoomed: Vec<f32> = wheel
        .iter()
        .filter_map(|m| if let Msg::ZoomBy(d) = m { Some(*d) } else { None })
        .collect();
    assert!(
        !zoomed.is_empty(),
        "a wheel notch over an EMPTY pane must return a ZoomBy — it returned {wheel:?}"
    );
    assert!(zoomed.iter().any(|d| *d != 0.0), "and the delta must be real: {zoomed:?}");

    eprintln!("[seam-17] empty pane: PanBy {panned:?}, ZoomBy {zoomed:?}");
    nornir_testmatrix::functional_status(
        "facett-map",
        "empty_pane_still_pans_and_zooms",
        !panned.is_empty() && !zoomed.is_empty(),
        &format!("PanBy {panned:?}, ZoomBy {zoomed:?}"),
    );
}

/// **NON-VACUOUS CONTROL.** The same gestures on a POPULATED pane have always worked —
/// so if this one ever fails the harness is broken, not the fix, and the assertion above
/// is not proving anything about `seam-17` at all.
#[test]
fn a_populated_pane_pans_too_so_the_empty_case_is_a_real_comparison() {
    let map = MapView::new("full", vec![(18.06, 59.33), (17.64, 59.85), (12.0, 57.7)]);

    let drag = drive(
        &map,
        vec![
            vec![
                egui::Event::PointerMoved(OVER_PANE),
                egui::Event::PointerButton {
                    pos: OVER_PANE,
                    button: egui::PointerButton::Primary,
                    pressed: true,
                    modifiers: Modifiers::default(),
                },
            ],
            vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(40.0, -25.0))],
            vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(70.0, -40.0))],
        ],
    );
    assert!(
        drag.iter().any(|m| matches!(m, Msg::PanBy(_))),
        "the control case must pan: {drag:?}"
    );
}