aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Teclado: keymap con contextos, acciones que suben por la ruta de foco, texto y Tab.

use crate::core::Action;
use crate::view::interactivity::{ActionEvent, ActionHandler, Effects, EventContext, KeyDownEvent, TextInputEvent};
use crate::view::keymap::KeymapMatch;
use crate::view::window::Window;

impl Window {
    fn focus_path(&self) -> Vec<usize> {
        self.frame.dispatch.path_to_focus(self.entities.focus.focused())
    }

    /// Orden: atajos del keymap, luego `on_key_down`, luego texto, luego Tab.
    pub fn dispatch_keystroke(&mut self, keystroke: &str, text: Option<&str>) -> bool {
        let path = self.focus_path();
        let contexts = self.frame.dispatch.context_stack(&path);
        self.pending_keys.push(keystroke.to_owned());
        let resolved = self.keymap.resolve(&self.pending_keys, &contexts);
        // Ayuda which-key: mientras el atajo está a medias, la app puede mostrar cómo sigue.
        self.entities.key_hint = (resolved == KeymapMatch::Pending).then(|| crate::core::KeyHint {
            prefix: self.pending_keys.join(" "),
            next: self.keymap.continuations(&self.pending_keys, &contexts),
        });
        match resolved {
            KeymapMatch::Pending => return true,
            KeymapMatch::Action(action) => {
                self.pending_keys.clear();
                if self.dispatch_action(action) {
                    return true;
                }
            }
            KeymapMatch::None => {
                let was_sequence = self.pending_keys.len() > 1;
                self.pending_keys.clear();
                if was_sequence {
                    return self.dispatch_keystroke(keystroke, text);
                }
            }
        }

        let event = KeyDownEvent { keystroke: keystroke.to_owned(), text: text.map(str::to_owned) };
        let key_handlers: Vec<_> =
            path.iter().rev().filter_map(|index| self.frame.dispatch.node(*index)?.key_down.clone()).collect();
        for handler in key_handlers {
            let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
            handler(&event, &mut context);
            let effects = context.effects;
            let stop = !effects.propagate;
            self.finish(effects);
            if stop {
                return true;
            }
        }

        let is_command = keystroke.contains("mod-") || keystroke.contains("ctrl-");
        if let Some(text) = text.filter(|text| !is_command && !text.is_empty() && !text.chars().any(char::is_control))
            && self.dispatch_text_input(TextInputEvent::Insert(text.to_owned()))
        {
            return true;
        }
        match keystroke {
            "tab" => self.cycle_focus(1),
            "shift-tab" => self.cycle_focus(-1),
            _ => false,
        }
    }

    /// Ejecuta la acción en el manejador más profundo de la ruta de foco que la atienda.
    pub fn dispatch_action(&mut self, action: Action) -> bool {
        let path = self.focus_path();
        let handlers: Vec<ActionHandler> = path
            .iter()
            .rev()
            .filter_map(|index| self.frame.dispatch.node(*index))
            .flat_map(|node| {
                node.actions.iter().filter(|(candidate, _)| *candidate == action).map(|(_, handler)| handler.clone())
            })
            .collect();
        for handler in handlers {
            let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
            handler(&ActionEvent { action }, &mut context);
            let effects = context.effects;
            let stop = !effects.propagate;
            self.finish(effects);
            if stop {
                return true;
            }
        }
        false
    }

    pub(crate) fn dispatch_text_input(&mut self, event: TextInputEvent) -> bool {
        let path = self.focus_path();
        let handler = path.iter().rev().find_map(|index| self.frame.dispatch.node(*index)?.text_input.clone());
        let Some(handler) = handler else { return false };
        let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
        handler(&event, &mut context);
        let effects = context.effects;
        self.finish(effects);
        true
    }

    /// Mueve el foco al siguiente (o anterior) elemento con `tab_index`.
    fn cycle_focus(&mut self, step: isize) -> bool {
        let order = self.frame.dispatch.tab_order();
        if order.is_empty() {
            return false;
        }
        let current = self.entities.focus.focused().and_then(|focused| order.iter().position(|id| *id == focused));
        let next = match current {
            Some(index) => (index as isize + step).rem_euclid(order.len() as isize) as usize,
            None if step > 0 => 0,
            None => order.len() - 1,
        };
        self.entities.focus.focus(&crate::core::FocusHandle { id: order[next] });
        true
    }
}