gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The patch editor window as a whole — draft, Apply, undo and redo — driven
//! by pointer and key events, as the app drives it.

mod bench;
mod session;

use egui::{Event, Key, Modifiers, vec2};
use gled_model::patching::Patch;
use session::Session;

#[test]
fn a_fresh_window_has_nothing_to_apply_or_undo() {
    let session = Session::new();
    assert!(!session.window.unapplied());
    assert!(!session.can_undo());
    assert!(!session.can_redo());
}

/// The reported sequence: a drag that moves a fixture is a change to apply,
/// and one undo takes exactly it back.
#[test]
fn a_moving_drag_is_applied_and_undone_whole() {
    let mut session = Session::new();
    let before = session.patch().clone();
    let from = session.on_screen(session.left);

    session.drag(from, from + vec2(60.0, 40.0), false);

    assert_ne!(*session.patch(), before, "the drag moved the fixture");
    assert!(session.window.unapplied(), "the move is there to apply");
    assert!(session.can_undo());
    assert!(!session.can_redo());

    let moved = session.patch().clone();
    session.undo();
    assert_eq!(
        *session.patch(),
        before,
        "one undo takes the whole move back"
    );
    assert!(!session.window.unapplied(), "nothing is left to apply");
    assert!(!session.can_undo());
    assert!(session.can_redo());

    session.redo();
    assert_eq!(*session.patch(), moved, "redo moves it again");
    assert!(session.window.unapplied());
    assert!(session.can_undo());
    assert!(!session.can_redo());
}

/// The same with the move to the start and the press in one frame, as XTest
/// delivered them.
#[test]
fn a_drag_pressed_in_the_frame_it_arrived_moves_the_fixture_under_the_press() {
    let mut session = Session::new();
    let before = session.patch().clone();
    let from = session.on_screen(session.left);

    session.drag(from, from + vec2(60.0, 40.0), true);

    let moved = &session.patch().topology.default_layout.members;
    assert_ne!(
        moved[&session.left], before.topology.default_layout.members[&session.left],
        "the fixture under the press moved"
    );
    assert_eq!(
        moved[&session.right], before.topology.default_layout.members[&session.right],
        "the other one stayed"
    );
    assert!(session.window.unapplied());
    session.undo();
    assert_eq!(*session.patch(), before);
    assert!(!session.window.unapplied());
}

/// A band pulled from the background over a fixture selects it and moves
/// nothing, even when the pointer is over the fixture by the time the drag is
/// recognised.
#[test]
fn a_band_onto_a_fixture_only_selects() {
    let mut session = Session::new();
    let before = session.patch().clone();
    let left = session.on_screen(session.left);
    // below the fixture, on the background
    let from = left + vec2(0.0, 120.0);

    session.drag(from, left, true);

    assert_eq!(*session.patch(), before, "nothing moved");
    assert!(!session.window.unapplied());
    assert!(!session.can_undo(), "a band leaves no undo step");
    assert!(
        session
            .window
            .state
            .selected_fixtures
            .contains(&session.left)
    );
}

/// A press on the background with the pointer already over a fixture when
/// egui recognises the drag — a slow frame swallowed several moves — still
/// pulls a band rather than moving that fixture.
#[test]
fn a_band_whose_first_frame_lands_on_a_fixture_only_selects() {
    let mut session = Session::new();
    let before = session.patch().clone();
    let left = session.on_screen(session.left);
    let from = left + vec2(0.0, 120.0);

    session.frame(vec![Event::PointerMoved(from), Session::button(from, true)]);
    // one slow frame: the pointer is on the fixture before the drag starts
    session.frame(vec![
        Event::PointerMoved(from + vec2(0.0, -60.0)),
        Event::PointerMoved(left),
    ]);
    for step in 1..=5 {
        session.frame(vec![Event::PointerMoved(left + vec2(step as f32, 0.0))]);
    }
    session.frame(vec![Session::button(left + vec2(5.0, 0.0), false)]);
    session.frame(vec![]);

    assert_eq!(*session.patch(), before, "nothing moved");
    assert!(!session.window.unapplied());
    assert!(!session.can_undo());
}

/// A frame that brings the press on one fixture and the pointer already over
/// another moves the one pressed on.
#[test]
fn a_drag_moves_the_fixture_pressed_on_not_the_one_under_the_pointer() {
    let mut session = Session::new();
    let before = session.patch().topology.default_layout.members.clone();
    let left = session.on_screen(session.left);
    let right = session.on_screen(session.right);

    session.frame(vec![Event::PointerMoved(left)]);
    session.frame(vec![
        Session::button(left, true),
        Event::PointerMoved(left + (right - left) * 0.5),
        Event::PointerMoved(right),
    ]);
    for step in 1..=5 {
        session.frame(vec![Event::PointerMoved(right + vec2(step as f32, 0.0))]);
    }
    session.frame(vec![Session::button(right + vec2(5.0, 0.0), false)]);
    session.frame(vec![]);

    let after = session.patch().topology.default_layout.members.clone();
    assert_ne!(
        after[&session.left], before[&session.left],
        "the pressed one moved"
    );
    assert_eq!(
        after[&session.right], before[&session.right],
        "the other stayed"
    );
    assert_eq!(
        session.window.state.selected_fixtures,
        [session.left].into_iter().collect(),
        "the pressed one is selected"
    );
}

/// Regression: Apply, undo and redo were drawn before the canvas changed the
/// draft, and a drag moved nothing in the frame egui recognised it. A frame
/// that moved a fixture showed Apply greyed out beside it, and kept showing
/// that while no further frame came.
#[test]
fn the_frame_that_starts_a_drag_moves_the_fixture_and_shows_apply() {
    let mut session = Session::new();
    let before = session.patch().topology.default_layout.members[&session.left];
    let from = session.on_screen(session.left);
    assert!(!session.frame(vec![Event::PointerMoved(from)]));
    session.frame(vec![Session::button(from, true)]);

    // far enough for egui to call it a drag
    let shown = session.frame(vec![Event::PointerMoved(from + vec2(10.0, 0.0))]);

    let moved = session.patch().topology.default_layout.members[&session.left];
    assert!(
        moved.pos.x > before.pos.x,
        "the fixture follows the pointer"
    );
    assert!(session.window.unapplied());
    assert!(shown, "the frame shows there is something to apply");

    let to = from + vec2(60.0, 40.0);
    let shown = session.frame(vec![
        Event::PointerMoved(to),
        Session::button(to, false),
        Event::PointerMoved(to + vec2(1.0, 0.0)),
    ]);
    assert!(session.window.unapplied());
    assert!(shown);
}

/// A drag that ends where it began changed nothing: the frame that lets go
/// shows nothing to apply.
#[test]
fn the_frame_that_lets_go_where_the_drag_began_shows_nothing_to_apply() {
    let mut session = Session::new();
    let from = session.on_screen(session.left);
    session.frame(vec![Event::PointerMoved(from)]);
    session.frame(vec![Session::button(from, true)]);
    for step in 1..=3 {
        session.frame(vec![Event::PointerMoved(
            from + vec2(10.0 * step as f32, 0.0),
        )]);
    }
    assert!(session.window.unapplied());

    let shown = session.frame(vec![
        Event::PointerMoved(from),
        Session::button(from, false),
    ]);

    assert!(!session.window.unapplied());
    assert!(
        !session.can_undo(),
        "a drag that changed nothing is no step"
    );
    assert!(!shown, "the frame shows there is nothing to apply");
}

/// Undoing the only change makes Apply grey in the frame that undoes it.
#[test]
fn the_frame_that_undoes_the_change_shows_nothing_to_apply() {
    let mut session = Session::new();
    let from = session.on_screen(session.left);
    session.drag(from, from + vec2(60.0, 40.0), false);
    assert!(session.frame(vec![]));

    let undo = Event::Key {
        key: Key::Z,
        physical_key: None,
        pressed: true,
        repeat: false,
        modifiers: Modifiers::COMMAND,
    };
    let shown = session.frame(vec![Event::ModifiersChanged(Modifiers::COMMAND), undo]);

    assert!(!session.window.unapplied());
    assert!(!shown, "the frame shows what it did");
}

/// The interface adopting the engine's project keeps the draft while the
/// show's patch is still the one it was taken from, and starts it over on
/// another.
#[test]
fn adopting_another_patch_starts_the_draft_over() {
    let mut session = Session::new();
    let taken = session.project().patch.clone();
    let from = session.on_screen(session.left);
    session.drag(from, from + vec2(60.0, 40.0), false);
    assert!(session.window.unapplied());

    session.adopt(taken);
    assert!(session.window.unapplied(), "the same patch keeps the draft");

    session.adopt(Patch::default());
    assert!(!session.window.unapplied(), "another patch starts it over");
    assert_eq!(*session.patch(), Patch::default());
}

/// Apply hands the draft to the interface's project and tells the sync once,
/// which then compares the patch with the show's.
#[test]
fn apply_hands_the_draft_over_once() {
    let mut session = Session::new();
    let from = session.on_screen(session.left);
    session.drag(from, from + vec2(60.0, 40.0), false);
    assert!(!session.window.take_patch_applied());

    session.apply();
    assert_eq!(session.project().patch, *session.patch());
    assert!(!session.window.unapplied());
    assert!(session.window.take_patch_applied());
    assert!(!session.window.take_patch_applied(), "once");

    // the engine confirms it: the draft stays
    session.adopt(session.project().patch.clone());
    assert!(session.window.draft.is_some());
    assert!(!session.window.unapplied());
}