concinnity-dev 0.19.119

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
//! EditorHook: the Behavior panel's keyboard. `frame_keys` reaches the frontmost
//! open panel only, so these bindings never compete with another panel's, and a
//! key is one press rather than a held state -- one press is one step.
//!
//! The palette, while it is open, is modal over everything below it:
//!   typing       narrows it to what the query answers, best match first
//!   Up / Down    move the highlighted option, bringing it into the window
//!   Enter        insert the highlighted option
//!   Escape       close the palette without picking
//! Characters reach the palette's own field, so the keys below act on the options
//! the query keeps rather than on the whole vocabulary. Narrowing puts the
//! highlight back at the top, which is what makes typing and then committing land
//! on the best answer. Escape closes outright rather than first clearing the
//! query: one press, one meaning, and Backspace already clears.
//!
//! Escape (its own `FrameInput` pulse rather than a `InputKey`) otherwise answers
//! whichever state is waiting on a press, most consequential first: an armed
//! removal is canceled, and failing that the field holding the keyboard gives
//! it up, the name field reverting to what the world holds.
//!
//! Enter commits the field holding the keyboard -- the name onto the open
//! behavior, or the value into the selected row. With no field focused it acts on
//! the selection instead: opening its palette in the outline and the chart, and
//! in the overview opening the behavior a card stands for (a trigger, variable,
//! or asset card has none).
//!
//! Tab steps to the next view, the same cycle the header's own button walks.
//!
//! Ctrl carries a list member between places, whole subtree and all:
//!   Ctrl+C       hold the selected member
//!   Ctrl+V       put it into the list the selection addresses, after the
//!                selected member or at the end of the list itself
//!   Ctrl+D       duplicate the selected member in place, leaving what is held
//! What is held outlives the open behavior, so a node can be carried to another
//! one. These act only while no field holds the keyboard, since a copy is about
//! the selected node rather than the text being typed.
//!
//! The arrows move the selection through whichever view is showing:
//!   Outline      Up / Down step one row
//!   Chart        Left / Right follow the chain, Up / Down cross between a
//!                branching node's stacked branches
//!   Overview     the same, over the map's cards
//! The name field is the asset's rather than the selection's, so it holds the
//! keyboard -- arrows and Tab alike -- until Enter or Escape gives it up. The
//! value field is the selection's and follows it from view to view, so it holds
//! only Left and Right, which are the caret's (`input::text_system`).

use concinnity_core::components::FrameInput;
use concinnity_core::components::InputKey;
use concinnity_core::ecs::World;

use super::EditorHook;
use crate::editor::behavior;
use crate::editor::behavior::navigate::{self, Dir};
use crate::editor::behavior::panel::{BehaviorAction, ViewMode};

fn direction(key: InputKey) -> Option<Dir> {
    Some(match key {
        InputKey::Up => Dir::Up,
        InputKey::Down => Dir::Down,
        InputKey::Left => Dir::Left,
        InputKey::Right => Dir::Right,
        _ => return None,
    })
}

impl EditorHook {
    pub(super) fn behavior_keys(&mut self, world: &mut World, input: &FrameInput) {
        if input.escape {
            self.cancel_behavior_key(world);
            return;
        }
        for press in input.key_presses() {
            // Enter commits and Tab toggles the view: a held key acts once.
            if press.repeat && matches!(press.key, InputKey::Enter | InputKey::Tab) {
                continue;
            }
            self.behavior_key(press.key, press.mods.ctrl, world);
        }
    }

    fn behavior_key(&mut self, key: InputKey, ctrl: bool, world: &mut World) {
        if self.behavior_palette_open() {
            self.behavior_palette_key(key, world);
            return;
        }
        // Ctrl carries the clipboard, and only while no field holds the keyboard:
        // a copy is about the selected node, not about the text being typed.
        if ctrl {
            if !self.behavior.focus && !self.behavior.name_focus {
                self.clipboard_behavior_key(key, world);
            }
            return;
        }
        match key {
            InputKey::Enter => self.commit_behavior_key(world),
            _ if self.behavior.name_focus => {}
            InputKey::Tab => {
                self.apply_behavior_action(BehaviorAction::ToggleView, world, [0.0, 0.0])
            }
            // Left and Right belong to the caret while the value field holds
            // the keyboard.
            _ => {
                let step = direction(key).filter(|d| !(self.behavior.focus && d.horizontal()));
                if let Some(dir) = step {
                    self.step_behavior_selection(dir, world);
                }
            }
        }
    }

    fn clipboard_behavior_key(&mut self, key: InputKey, world: &mut World) {
        let action = match key {
            InputKey::C => BehaviorAction::Copy,
            InputKey::V => BehaviorAction::Paste,
            InputKey::D => BehaviorAction::Duplicate,
            _ => return,
        };
        self.apply_behavior_action(action, world, [0.0, 0.0]);
    }

    // Whether the palette is not just up but showing something to pick, which is
    // the same test the panel draws and hit-tests by.
    fn behavior_palette_open(&self) -> bool {
        self.behavior.picking && !self.behavior_data().picks.is_empty()
    }

    fn behavior_palette_key(&mut self, key: InputKey, world: &mut World) {
        match key {
            InputKey::Enter => {
                // The highlight counts kept options, so it resolves through the
                // filter before it can name one.
                let Some(&at) = self.behavior_data().matches.get(self.behavior.pick) else {
                    return;
                };
                self.apply_behavior_action(BehaviorAction::Choose(at), world, [0.0, 0.0]);
            }
            InputKey::Up | InputKey::Down => {
                let total = self.behavior_data().matches.len();
                let delta = if key == InputKey::Up { -1 } else { 1 };
                let Some(at) = navigate::step(Some(self.behavior.pick), delta, total) else {
                    return;
                };
                self.behavior.pick = at;
                self.behavior.pick_scroll =
                    navigate::scroll_to(at, self.behavior.pick_scroll, behavior::panel::PICK_POOL);
            }
            _ => {}
        }
    }

    fn cancel_behavior_key(&mut self, world: &mut World) {
        if self.behavior_palette_open() {
            self.apply_behavior_action(BehaviorAction::Dismiss, world, [0.0, 0.0]);
            return;
        }
        if std::mem::take(&mut self.behavior.remove_armed) {
            return;
        }
        if self.behavior.name_focus {
            self.blur_behavior_name(world);
            return;
        }
        self.behavior.focus = false;
    }

    fn commit_behavior_key(&mut self, world: &mut World) {
        if self.behavior.name_focus {
            self.rename_behavior(world);
            return;
        }
        if self.behavior.focus {
            self.commit_behavior_value(world);
            return;
        }
        let action = match self.behavior.mode {
            // Whatever the card stands for, reached the way a press on it
            // reaches it.
            ViewMode::Overview => {
                let data = self.behavior_data();
                let on = self
                    .behavior
                    .overview_card
                    .and_then(|i| Some((i, data.overview.cards.get(i)?)));
                let Some((i, card)) = on else {
                    return;
                };
                behavior::panel::overview_action(card, i)
            }
            // A row offering nothing has no palette to open, so the press is
            // left alone rather than toggling one that never shows.
            _ if self.behavior.row.is_some() && !self.behavior_data().picks.is_empty() => {
                BehaviorAction::Palette
            }
            _ => return,
        };
        self.apply_behavior_action(action, world, [0.0, 0.0]);
    }

    fn step_behavior_selection(&mut self, dir: Dir, world: &mut World) {
        match self.behavior.mode {
            ViewMode::Outline => {
                let Some(delta) = dir.list_step() else {
                    return;
                };
                let total = self.behavior_rows().len();
                let Some(row) = navigate::step(self.behavior.row, delta, total) else {
                    return;
                };
                self.apply_behavior_action(BehaviorAction::Select(row), world, [0.0, 0.0]);
            }
            ViewMode::Chart => {
                let data = self.behavior_data();
                // Every card is reachable: the pan that follows brings the one
                // stepped onto into the canvas, whether or not it was drawn.
                let Some(card) = navigate::nearest(&data.chart.cards, data.card, dir) else {
                    return;
                };
                self.apply_behavior_action(BehaviorAction::SelectCard(card), world, [0.0, 0.0]);
            }
            ViewMode::Overview => {
                let data = self.behavior_data();
                let cards = &data.overview.cards;
                let from = self.behavior.overview_card.filter(|&i| i < cards.len());
                let Some(card) = navigate::nearest(cards, from, dir) else {
                    return;
                };
                self.behavior.overview_card = Some(card);
            }
        }
        self.ensure_behavior_visible();
    }
}