concinnity-dev 0.19.119

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
//! The command palette's drive (`hook/edit/palette.rs`): the shortcut that opens
//! it under either platform modifier, the query mirrored off its field, the
//! keyboard walk over the matches, and what committing a row does. The ranking
//! and the providers are tested beside them in `editor/palette/`.

use concinnity_core::components::FrameInput;
use concinnity_core::components::InputKey;
use concinnity_core::components::KeyEvent;
use concinnity_core::components::KeyMods;
use concinnity_core::components::KeyPress;
use concinnity_core::components::TextInput;
use concinnity_core::ecs::World;

use crate::editor::hook::EditorHook;
use crate::editor::palette;
use crate::editor::palette::{Category, PaletteAction};
use crate::editor::panels::registry::PanelKey;
use crate::editor::widget;

fn hook(entries: Vec<serde_json::Value>) -> EditorHook {
    EditorHook::new("unused.jsonl".to_string(), entries)
}

// A world holding the palette's query field.
fn palette_world() -> World {
    let mut world = World::new();
    for id in palette::panel::ids().field_ids() {
        world.push_identified(id, TextInput::default());
    }
    world
}

fn chord(key: InputKey, ctrl: bool, cmd: bool) -> FrameInput {
    FrameInput {
        key_events: vec![KeyEvent::Press(KeyPress::new(
            key,
            KeyMods {
                ctrl,
                cmd,
                ..KeyMods::NONE
            },
        ))],
        ctrl,
        cmd,
        ..Default::default()
    }
}

// Both platform modifiers open the palette: Ctrl on Windows and Linux, Command
// on macOS, where `cmd` is the only one the backend sets.
#[test]
fn either_platform_modifier_opens_the_palette() {
    for (ctrl, cmd) in [(true, false), (false, true)] {
        let mut h = hook(Vec::new());
        let mut world = palette_world();
        h.drive_palette_toggle(&chord(InputKey::K, ctrl, cmd), &mut world);
        assert!(
            h.open[PanelKey::Palette],
            "ctrl={ctrl} cmd={cmd} did not open it"
        );
        // The same chord closes it again.
        h.drive_palette_toggle(&chord(InputKey::K, ctrl, cmd), &mut world);
        assert!(!h.open[PanelKey::Palette]);
    }
}

// A bare K types a letter; only the modified chord is the shortcut.
#[test]
fn an_unmodified_k_leaves_the_palette_closed() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, false, false), &mut world);
    assert!(!h.open[PanelKey::Palette]);
}

// Opening blurs the field for one frame, so the keypress that opened it cannot
// also be typed into the fresh query.
#[test]
fn opening_blurs_the_query_for_one_frame() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);
    assert!(h.palette.blur);
    assert!(!h.make_palette_view([0.0, 0.0]).focus, "blurred this frame");
}

// The empty query offers the panels and commands; typing narrows across the
// categories and re-homes the highlight to the best answer.
#[test]
fn typing_reranks_and_rehomes_the_highlight() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);
    let opened = h.palette.matches.len();
    assert!(opened > 0, "the launch list is not empty");

    h.palette.pick = 3;
    widget::seed_field(&mut world, palette::panel::INPUT, "cook");
    h.sample_palette_query(&world);
    assert_eq!(
        h.palette.pick, 0,
        "a narrowed list starts at its best answer"
    );
    assert!(h.palette.matches.len() < opened);
    let first = &h.palette.items[h.palette.matches[0]];
    assert_eq!(first.label, "/cook");
}

// Up / Down walk the matches and stop at either end rather than wrapping.
#[test]
fn the_arrows_walk_the_matches_and_stop_at_the_ends() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);

    h.palette_keys(&mut world, &chord(InputKey::Down, false, false));
    assert_eq!(h.palette.pick, 1);
    h.palette_keys(&mut world, &chord(InputKey::Up, false, false));
    assert_eq!(h.palette.pick, 0);
    h.palette_keys(&mut world, &chord(InputKey::Up, false, false));
    assert_eq!(h.palette.pick, 0, "stops at the top");
}

// Committing a panel row opens that panel and closes the palette.
#[test]
fn committing_a_panel_row_opens_it() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);
    let at = h
        .palette
        .matches
        .iter()
        .position(|&i| h.palette.items[i].action == PaletteAction::OpenPanel(PanelKey::Variables))
        .expect("the Variables panel is a palette row");
    h.palette.pick = at;

    h.palette_keys(&mut world, &chord(InputKey::Enter, false, false));
    assert!(!h.open[PanelKey::Palette], "committing closes the palette");
    assert!(h.open[PanelKey::Variables], "the panel opened");
    assert_eq!(
        h.panel_order.last().copied(),
        Some(PanelKey::Variables),
        "and it is frontmost"
    );
}

// A command taking arguments seeds command mode instead of running: the
// palette stays up with the name typed, ready for its arguments.
#[test]
fn committing_an_argument_command_seeds_command_mode() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);
    let at = h
        .palette
        .matches
        .iter()
        .position(|&i| h.palette.items[i].label == "/add")
        .expect("/add is a palette row");
    h.palette.pick = at;

    h.palette_keys(&mut world, &chord(InputKey::Enter, false, false));
    assert!(
        h.open[PanelKey::Palette],
        "the palette stays up for the arguments"
    );
    assert_eq!(widget::field_text(&world, palette::panel::INPUT), "/add ");
}

// A committed row leads the next empty-query list, so repeating recent work is
// one keystroke away.
#[test]
fn a_commit_is_remembered_for_the_next_launch_list() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);
    let at = h
        .palette
        .matches
        .iter()
        .position(|&i| h.palette.items[i].action == PaletteAction::OpenPanel(PanelKey::Variables))
        .expect("the Variables panel is a palette row");
    let label = h.palette.items[h.palette.matches[at]].label.clone();
    h.palette.pick = at;
    h.palette_keys(&mut world, &chord(InputKey::Enter, false, false));

    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);
    let first = &h.palette.items[h.palette.matches[0]];
    assert_eq!(first.label, label, "the last commit leads the list");
}

// Escape closes the palette through the global escape drive, and a click
// outside it dismisses while claiming the press, so nothing underneath is
// picked by the dismissal.
#[test]
fn a_press_outside_dismisses_without_reaching_the_world() {
    let mut h = hook(Vec::new());
    let mut world = palette_world();
    let vp = [1280.0, 720.0];
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);

    let o = h.origin(PanelKey::Palette, vp);
    let inside = FrameInput {
        mouse_x: o[0] + 20.0,
        mouse_y: o[1] + 10.0,
        ..Default::default()
    };
    assert!(
        !h.route_palette_dismiss(&inside, vp),
        "a press on the palette is left to its own hit test"
    );
    assert!(h.open[PanelKey::Palette]);

    let outside = FrameInput {
        mouse_x: o[0] - 40.0,
        mouse_y: o[1] + 10.0,
        ..Default::default()
    };
    assert!(
        h.route_palette_dismiss(&outside, vp),
        "the press is claimed"
    );
    assert!(!h.open[PanelKey::Palette]);
}

// The world's assets reach the palette: a behavior is offered as an asset row
// that opens its own panel, everything else as an entity row.
#[test]
fn world_assets_become_rows_routed_by_type() {
    let entries = vec![
        serde_json::json!({"type": "Behavior", "args": {"$id": "greeter", "on": "tick", "do": []}}),
    ];
    let mut h = hook(entries);
    let mut world = palette_world();
    h.drive_palette_toggle(&chord(InputKey::K, true, false), &mut world);

    let row = h
        .palette
        .items
        .iter()
        .find(|it| it.label == "greeter")
        .expect("the authored behavior is a palette row");
    assert_eq!(row.category, Category::Asset);
    assert_eq!(row.action, PaletteAction::OpenAsset("greeter".to_string()));
}