aura-gui 0.3.5

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.
//! Ratón, rueda e IME: localizar el elemento bajo el puntero y avisarle.

use crate::core::{CursorStyle, HitboxId, InputEvent, MouseButton, Point};
use crate::view::interactivity::{
    ClickEvent, DragEvent, Effects, EventContext, ScrollEvent, ScrollState, TextInputEvent,
};
use crate::view::window::Window;
use std::time::{Duration, Instant};

const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(450);
const DOUBLE_CLICK_DISTANCE: f32 = 4.0;

impl Window {
    /// Devuelve `true` si hay que volver a dibujar.
    pub fn handle_event(&mut self, event: &InputEvent) -> bool {
        match event {
            InputEvent::MouseMove { position } => self.on_mouse_move(*position),
            InputEvent::MouseDown { position, button } => self.on_mouse_down(*position, *button),
            InputEvent::MouseUp { position, button } => {
                // Un arrastre que llegó a empezar se suelta en vez de hacer clic.
                if let Some(redraw) = self.finish_drag(*position) {
                    self.mouse.pressed = None;
                    self.pressed_drag = None;
                    return redraw;
                }
                let click = ClickEvent {
                    position: *position,
                    button: *button,
                    modifiers: self.modifiers,
                    click_count: self.click_count(),
                };
                let redraw = *button == MouseButton::Left && self.dispatch_click(&click, ClickKind::Up);
                let was_pressed = self.mouse.pressed.take().is_some();
                self.pressed_drag = None;
                redraw | was_pressed
            }
            InputEvent::Scroll { position, delta } => {
                self.dispatch_scroll(&ScrollEvent { position: *position, delta: *delta })
            }
            InputEvent::KeyDown { keystroke, text } => self.dispatch_keystroke(keystroke, text.as_deref()),
            InputEvent::ImePreedit { text, cursor } => {
                self.dispatch_text_input(TextInputEvent::Preedit { text: text.clone(), cursor: *cursor })
            }
            InputEvent::ImeCommit(text) => self.dispatch_text_input(TextInputEvent::Insert(text.clone())),
        }
    }

    fn click_count(&self) -> u32 {
        self.last_click.map_or(1, |(_, _, count)| count)
    }

    fn on_mouse_down(&mut self, position: Point, button: MouseButton) -> bool {
        let now = Instant::now();
        let count = match self.last_click {
            Some((at, last, count))
                if now.duration_since(at) < DOUBLE_CLICK_INTERVAL
                    && (position.x.0 - last.x.0).abs() < DOUBLE_CLICK_DISTANCE
                    && (position.y.0 - last.y.0).abs() < DOUBLE_CLICK_DISTANCE =>
            {
                count % 3 + 1
            }
            _ => 1,
        };
        self.last_click = Some((now, position, count));
        self.mouse.pressed = self.frame.hitboxes.topmost_at(position);
        self.pressed_drag = self
            .mouse
            .pressed
            .and_then(|pressed| self.frame.handlers.iter().find(|entry| entry.hitbox == pressed)?.on_drag.clone());
        self.focus_hit(position);
        if button == MouseButton::Left {
            self.prepare_drag(position);
        }
        let click = ClickEvent { position, button, modifiers: self.modifiers, click_count: count };
        let kind = if button == MouseButton::Right { ClickKind::Right } else { ClickKind::Down };
        self.dispatch_click(&click, kind) | self.mouse.pressed.is_some()
    }

    /// El clic enfoca el elemento enfocable más alto bajo el puntero.
    fn focus_hit(&mut self, position: Point) {
        let hits: Vec<HitboxId> = self.frame.hitboxes.hits_at(position).collect();
        let focus = hits.iter().find_map(|hit| self.frame.handlers.iter().find(|entry| entry.hitbox == *hit)?.focus);
        if let Some(focus) = focus {
            self.entities.focus.focus(&focus);
        }
    }

    fn on_mouse_move(&mut self, position: Point) -> bool {
        let dragged = self.update_drag(position) || self.dispatch_drag(position);
        let moved = self.dispatch_move(position);
        let before: Vec<HitboxId> = self.frame.hitboxes.hits_at(self.mouse.position).collect();
        self.mouse.position = position;
        let after: Vec<HitboxId> = self.frame.hitboxes.hits_at(position).collect();
        let tooltip_changed = self.track_tooltip();
        dragged | moved | tooltip_changed | (before != after)
    }

    /// Movimiento sin botón presionado: lo recibe el elemento más alto con manejador.
    fn dispatch_move(&mut self, position: Point) -> bool {
        if self.mouse.pressed.is_some() {
            return false;
        }
        let handler = self
            .frame
            .hitboxes
            .hits_at(position)
            .find_map(|hit| self.frame.handlers.iter().find(|entry| entry.hitbox == hit)?.on_mouse_move.clone());
        let Some(handler) = handler else { return false };
        let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
        handler(&DragEvent { position, modifiers: self.modifiers }, &mut context);
        let effects = context.effects;
        self.finish(effects)
    }

    fn dispatch_drag(&mut self, position: Point) -> bool {
        if self.mouse.pressed.is_none() {
            return false;
        }
        let Some(handler) = self.pressed_drag.clone() else { return false };
        let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
        handler(&DragEvent { position, modifiers: self.modifiers }, &mut context);
        let effects = context.effects;
        self.finish(effects)
    }

    fn dispatch_click(&mut self, event: &ClickEvent, kind: ClickKind) -> bool {
        let hits: Vec<HitboxId> = self.frame.hitboxes.hits_at(event.position).collect();
        let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
        for hit in hits {
            let Some(entry) = self.frame.handlers.iter().find(|entry| entry.hitbox == hit) else { continue };
            let handler = match kind {
                ClickKind::Down => &entry.on_mouse_down,
                ClickKind::Right => &entry.on_right_click,
                // Un clic solo cuenta si empezó y terminó sobre el mismo elemento.
                ClickKind::Up if self.mouse.pressed == Some(hit) => &entry.on_click,
                ClickKind::Up => &None,
            };
            if let Some(handler) = handler.clone() {
                context.effects.propagate = false;
                handler(event, &mut context);
                if !context.effects.propagate {
                    break;
                }
            }
        }
        let effects = context.effects;
        self.finish(effects)
    }

    fn dispatch_scroll(&mut self, event: &ScrollEvent) -> bool {
        let hits: Vec<HitboxId> = self.frame.hitboxes.hits_at(event.position).collect();
        let mut context = EventContext { entities: &mut self.entities, effects: Effects::default() };
        for hit in hits {
            let Some(entry) = self.frame.handlers.iter().find(|entry| entry.hitbox == hit) else { continue };
            if let Some(handler) = entry.on_scroll.clone() {
                handler(event, &mut context);
                break;
            }
            if let Some(scroll) = &entry.scroll {
                // Un contenedor al tope deja pasar la rueda a su ancestro.
                if ScrollState::scroll_by(scroll, event.delta.y.0) {
                    context.effects.notify();
                    break;
                }
            }
        }
        let effects = context.effects;
        self.finish(effects)
    }

    /// Forma del puntero: la del elemento presionado mientras se arrastra, si no la
    /// del elemento más alto bajo el ratón que pida una.
    pub fn cursor_style(&self) -> CursorStyle {
        if self.active_drag_type().is_some() {
            return CursorStyle::Grabbing;
        }
        let cursor_of = |hit: HitboxId| self.frame.handlers.iter().find(|entry| entry.hitbox == hit)?.cursor;
        if let Some(cursor) = self.mouse.pressed.and_then(cursor_of) {
            return cursor;
        }
        self.frame.hitboxes.hits_at(self.mouse.position).find_map(cursor_of).unwrap_or_default()
    }

    /// Aplica los efectos de un manejador: acciones encoladas y necesidad de redibujar.
    pub(crate) fn finish(&mut self, effects: Effects) -> bool {
        let mut redraw = effects.redraw;
        let mut actions = effects.actions;
        actions.append(&mut self.entities.pending_actions);
        for action in actions {
            redraw |= self.dispatch_action(action);
        }
        redraw || self.entities.is_dirty()
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum ClickKind {
    Down,
    Up,
    Right,
}