envision 0.16.0

A ratatui framework for collaborative TUI development with headless testing support
Documentation
//! Overlay trait definition.

use crate::component::RenderContext;
use crate::input::Event;

use super::OverlayAction;

/// A modal overlay that can intercept events and render on top of the main view.
///
/// Overlays own their transient UI state (search query, cursor position, scroll
/// offset) via `&mut self` on `handle_event`. The `Send` bound enables future
/// async compatibility.
///
/// # Example
///
/// ```rust
/// use envision::overlay::{Overlay, OverlayAction};
/// use envision::component::RenderContext;
/// use envision::input::{Event, Key};
///
/// struct ConfirmDialog {
///     message: String,
/// }
///
/// impl Overlay<String> for ConfirmDialog {
///     fn handle_event(&mut self, event: &Event) -> OverlayAction<String> {
///         if let Some(key) = event.as_key() {
///             match key.code {
///                 Key::Char('y') => OverlayAction::DismissWithMessage("confirmed".into()),
///                 Key::Char('n') | Key::Esc => OverlayAction::Dismiss,
///                 _ => OverlayAction::Consumed,
///             }
///         } else {
///             OverlayAction::Propagate
///         }
///     }
///
///     fn view(&self, ctx: &mut RenderContext<'_, '_>) {
///         // Render the confirmation dialog using ctx.frame, ctx.area, ctx.theme
///     }
/// }
/// ```
pub trait Overlay<M>: Send {
    /// Handle an input event.
    ///
    /// The overlay can mutate itself (e.g., update a search buffer).
    fn handle_event(&mut self, event: &Event) -> OverlayAction<M>;

    /// Render the overlay on top of the main view.
    fn view(&self, ctx: &mut RenderContext<'_, '_>);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::component::RenderContext;
    use crate::input::Key;

    struct TestOverlay {
        consumed_count: u32,
    }

    impl Overlay<String> for TestOverlay {
        fn handle_event(&mut self, event: &Event) -> OverlayAction<String> {
            if let Some(key) = event.as_key() {
                match key.code {
                    Key::Esc => OverlayAction::Dismiss,
                    Key::Enter => OverlayAction::DismissWithMessage("confirmed".to_string()),
                    _ => {
                        self.consumed_count += 1;
                        OverlayAction::Consumed
                    }
                }
            } else {
                OverlayAction::Propagate
            }
        }

        fn view(&self, _ctx: &mut RenderContext<'_, '_>) {
            // no-op for testing
        }
    }

    #[test]
    fn test_overlay_handle_event_consumed() {
        let mut overlay = TestOverlay { consumed_count: 0 };
        let event = Event::char('a');

        let action = overlay.handle_event(&event);
        assert!(matches!(action, OverlayAction::Consumed));
        assert_eq!(overlay.consumed_count, 1);
    }

    #[test]
    fn test_overlay_handle_event_dismiss() {
        let mut overlay = TestOverlay { consumed_count: 0 };
        let event = Event::key(Key::Esc);

        let action = overlay.handle_event(&event);
        assert!(matches!(action, OverlayAction::Dismiss));
    }

    #[test]
    fn test_overlay_handle_event_dismiss_with_message() {
        let mut overlay = TestOverlay { consumed_count: 0 };
        let event = Event::key(Key::Enter);

        let action = overlay.handle_event(&event);
        assert!(matches!(action, OverlayAction::DismissWithMessage(ref s) if s == "confirmed"));
    }

    #[test]
    fn test_overlay_handle_event_propagate() {
        let mut overlay = TestOverlay { consumed_count: 0 };
        let event = Event::Resize(80, 24);

        let action = overlay.handle_event(&event);
        assert!(matches!(action, OverlayAction::Propagate));
    }
}