bubba-core 0.2.2

Core runtime for the Bubba mobile framework
Documentation
//! # Events
//!
//! Bubba's event system. Every interactive element supports a small set of
//! declarative events that map directly to native Android touch/input callbacks.
//!
//! ## Supported Events
//! | Attribute  | Fires when …                              |
//! |------------|-------------------------------------------|
//! | `onclick`  | Element is tapped / clicked               |
//! | `oninput`  | Text input changes (per character)        |
//! | `onkeypress` | A key is pressed in a focused input     |
//! | `onfocus`  | Element receives focus                    |
//! | `onblur`   | Element loses focus                       |
//! | `onchange` | Select / checkbox value changes           |

use std::sync::Arc;

/// The raw event payload passed to every handler.
#[derive(Debug, Clone)]
pub struct BubbaEvent {
    /// The event name ("click", "input", "keypress", …).
    pub kind: &'static str,
    /// Optional string payload (e.g. the current value of an `<input>`).
    pub value: Option<String>,
    /// Optional key name for keyboard events.
    pub key: Option<String>,
}

/// A boxed, type-erased event callback.
pub type Callback = Arc<dyn Fn(BubbaEvent) + Send + Sync + 'static>;

/// An event handler attached to a UI element.
#[derive(Clone)]
pub struct EventHandler {
    /// The event name this handler listens to ("click", "input", …).
    pub event: &'static str,
    /// The callback invoked when the event fires.
    pub callback: Callback,
}

impl std::fmt::Debug for EventHandler {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EventHandler")
            .field("event", &self.event)
            .field("callback", &"<fn>")
            .finish()
    }
}

impl EventHandler {
    /// Create a new event handler.
    pub fn new<F>(event: &'static str, f: F) -> Self
    where
        F: Fn(BubbaEvent) + Send + Sync + 'static,
    {
        Self {
            event,
            callback: Arc::new(f),
        }
    }

    /// Convenience: create an `onclick` handler.
    pub fn onclick<F: Fn(BubbaEvent) + Send + Sync + 'static>(f: F) -> Self {
        Self::new("click", f)
    }

    /// Convenience: create an `oninput` handler.
    pub fn oninput<F: Fn(BubbaEvent) + Send + Sync + 'static>(f: F) -> Self {
        Self::new("input", f)
    }

    /// Convenience: create an `onkeypress` handler.
    pub fn onkeypress<F: Fn(BubbaEvent) + Send + Sync + 'static>(f: F) -> Self {
        Self::new("keypress", f)
    }

    /// Convenience: create an `onfocus` handler.
    pub fn onfocus<F: Fn(BubbaEvent) + Send + Sync + 'static>(f: F) -> Self {
        Self::new("focus", f)
    }

    /// Convenience: create an `onblur` handler.
    pub fn onblur<F: Fn(BubbaEvent) + Send + Sync + 'static>(f: F) -> Self {
        Self::new("blur", f)
    }

    /// Dispatch this handler with a given event payload.
    pub fn dispatch(&self, event: BubbaEvent) {
        (self.callback)(event);
    }
}

/// Global event dispatcher — the runtime calls this when Android fires an event.
pub struct EventDispatcher {
    /// Pending events waiting to be processed.
    queue: std::sync::Mutex<Vec<(String, BubbaEvent)>>,
}

impl EventDispatcher {
    /// Create a new dispatcher.
    pub fn new() -> Self {
        Self {
            queue: std::sync::Mutex::new(Vec::new()),
        }
    }

    /// Push a new event into the queue.
    pub fn push(&self, element_id: impl Into<String>, event: BubbaEvent) {
        let mut q = self.queue.lock().unwrap();
        q.push((element_id.into(), event));
    }

    /// Drain and process all pending events.
    pub fn flush(&self, handlers: &[(String, EventHandler)]) {
        let events: Vec<(String, BubbaEvent)> = {
            let mut q = self.queue.lock().unwrap();
            std::mem::take(&mut *q)
        };

        for (element_id, event) in events {
            for (handler_id, handler) in handlers {
                if *handler_id == element_id && handler.event == event.kind {
                    handler.dispatch(event.clone());
                }
            }
        }
    }
}

impl Default for EventDispatcher {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicBool, Ordering};

    #[test]
    fn handler_fires() {
        let fired = Arc::new(AtomicBool::new(false));
        let fired_clone = Arc::clone(&fired);

        let h = EventHandler::onclick(move |_e| {
            fired_clone.store(true, Ordering::SeqCst);
        });

        h.dispatch(BubbaEvent {
            kind: "click",
            value: None,
            key: None,
        });

        assert!(fired.load(Ordering::SeqCst));
    }
}