use std::sync::Arc;
use kimun_core::NoteVault;
use ratatui::Frame;
use ratatui::layout::Rect;
use std::time::Instant;
use crate::components::event_state::EventState;
use crate::components::events::{AppTx, InputEvent, OverlayData};
use crate::components::overlay::{Overlay, OverlayKind};
use crate::components::sortable::SortableList;
use crate::settings::themes::Theme;
pub struct OverlayHost<F> {
active: Option<Box<dyn Overlay>>,
parked: Option<Box<dyn Overlay>>,
saved_focus: Option<F>,
guard: crate::components::clickable::OpenGuard,
}
impl<F> OverlayHost<F> {
pub fn new() -> Self {
Self {
active: None,
parked: None,
saved_focus: None,
guard: Default::default(),
}
}
pub fn is_open(&self) -> bool {
self.active.is_some()
}
pub fn active_kind(&self) -> Option<OverlayKind> {
self.active.as_ref().map(|o| o.kind())
}
pub fn active_query(&self) -> Option<&str> {
self.active.as_ref().and_then(|o| o.query())
}
pub fn active_saved_search_provenance(&self) -> Option<&str> {
self.active
.as_ref()
.and_then(|o| o.saved_search_provenance())
}
pub fn open(&mut self, overlay: Box<dyn Overlay>, panel_token: F) {
if self.saved_focus.is_none() {
self.saved_focus = Some(panel_token);
}
self.parked = None;
self.active = Some(overlay);
self.guard.opened(Instant::now());
}
pub fn open_over(&mut self, overlay: Box<dyn Overlay>) {
self.parked = self.active.take();
self.active = Some(overlay);
self.guard.opened(Instant::now());
}
pub fn parked_kind(&self) -> Option<OverlayKind> {
self.parked.as_ref().map(|o| o.kind())
}
pub fn active_sortable(&self) -> Option<&dyn SortableList> {
self.active.as_ref().and_then(|o| o.as_sortable())
}
pub fn active_takes_query_syntax(&self) -> bool {
self.active.as_ref().is_some_and(|o| o.takes_query_syntax())
}
pub fn parked_sortable_mut(&mut self) -> Option<&mut dyn SortableList> {
self.parked.as_mut().and_then(|o| o.as_sortable_mut())
}
pub fn close(&mut self) -> Option<F> {
self.guard.closed();
if let Some(parked) = self.parked.take() {
self.active = Some(parked);
return None;
}
self.active = None;
self.saved_focus.take()
}
pub fn close_all(&mut self) -> Option<F> {
self.guard.closed();
self.parked = None;
self.active = None;
self.saved_focus.take()
}
pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
self.handle_input_at(event, tx, Instant::now())
}
pub fn observe_input(&mut self, event: &InputEvent) {
self.guard.observe_input(event);
}
fn handle_input_at(&mut self, event: &InputEvent, tx: &AppTx, now: Instant) -> EventState {
if self.active.is_some() && self.guard.swallows(event, now) {
return EventState::Consumed;
}
if let Some(o) = &mut self.active {
o.handle_input(event, tx)
} else {
EventState::NotConsumed
}
}
pub fn handle_data(&mut self, data: &OverlayData, vault: &Arc<NoteVault>, tx: &AppTx) {
if let Some(o) = &mut self.active {
o.handle_data(data, vault, tx);
}
}
pub fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme) {
if let Some(o) = &mut self.parked {
o.render(f, area, theme);
}
if let Some(o) = &mut self.active {
crate::components::clickable::clear();
o.render(f, area, theme);
}
}
pub fn hint_shortcuts(&self) -> Vec<(String, String)> {
self.active
.as_ref()
.map(|o| o.hint_shortcuts())
.unwrap_or_default()
}
}
impl<F> Default for OverlayHost<F> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::layout::Rect;
struct FakeOverlay(OverlayKind);
impl Overlay for FakeOverlay {
fn kind(&self) -> OverlayKind {
self.0
}
fn handle_input(&mut self, _e: &InputEvent, _tx: &AppTx) -> EventState {
EventState::Consumed
}
fn render(&mut self, _f: &mut Frame, _a: Rect, _t: &Theme) {}
}
struct Echo;
impl Overlay for Echo {
fn kind(&self) -> OverlayKind {
OverlayKind::Dialog
}
fn handle_input(&mut self, _e: &InputEvent, tx: &AppTx) -> EventState {
tx.send(crate::components::events::AppEvent::CloseOverlay)
.ok();
EventState::Consumed
}
fn render(&mut self, _f: &mut Frame, _a: Rect, _t: &Theme) {}
}
#[test]
fn a_keyboard_open_takes_clicks_at_once() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut host: OverlayHost<u8> = OverlayHost::new();
host.observe_input(&InputEvent::Key(ratatui::crossterm::event::KeyEvent::from(
ratatui::crossterm::event::KeyCode::Char('p'),
)));
host.open(Box::new(Echo), 0);
host.handle_input_at(
&crate::test_support::mouse_down_at(0, 0),
&tx,
Instant::now(),
);
assert!(rx.try_recv().is_ok());
}
#[test]
fn a_press_right_after_opening_is_the_opening_gesture() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut host: OverlayHost<u8> = OverlayHost::new();
let press = crate::test_support::mouse_down_at(0, 0);
host.observe_input(&press);
let opened = Instant::now();
host.open(Box::new(Echo), 0);
let early = opened + crate::components::DOUBLE_CLICK / 2;
assert_eq!(
host.handle_input_at(&press, &tx, early),
EventState::Consumed
);
assert!(rx.try_recv().is_err(), "swallowed inside the window");
let key = InputEvent::Key(ratatui::crossterm::event::KeyEvent::from(
ratatui::crossterm::event::KeyCode::Esc,
));
host.handle_input_at(&key, &tx, early);
assert!(rx.try_recv().is_ok(), "keys are never held back");
let late = Instant::now() + crate::components::DOUBLE_CLICK;
host.handle_input_at(&press, &tx, late);
assert!(rx.try_recv().is_ok(), "delivered once the window passes");
}
#[test]
fn new_is_closed() {
let host: OverlayHost<u8> = OverlayHost::new();
assert!(!host.is_open());
assert_eq!(host.active_kind(), None);
}
#[test]
fn open_saves_focus_and_close_restores_it() {
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(FakeOverlay(OverlayKind::NoteBrowser)), 1);
assert!(host.is_open());
assert_eq!(host.active_kind(), Some(OverlayKind::NoteBrowser));
assert_eq!(host.close(), Some(1));
assert!(!host.is_open());
}
#[test]
fn chained_open_preserves_first_focus_token() {
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(FakeOverlay(OverlayKind::NoteBrowser)), 1);
host.open(Box::new(FakeOverlay(OverlayKind::Dialog)), 99);
assert_eq!(host.active_kind(), Some(OverlayKind::Dialog));
assert_eq!(host.close(), Some(1));
}
#[test]
fn close_when_empty_returns_none() {
let mut host: OverlayHost<u8> = OverlayHost::new();
assert_eq!(host.close(), None);
}
#[test]
fn open_over_parks_the_active_overlay() {
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(FakeOverlay(OverlayKind::NoteBrowser)), 1);
host.open_over(Box::new(FakeOverlay(OverlayKind::Dialog)));
assert_eq!(host.active_kind(), Some(OverlayKind::Dialog));
assert_eq!(host.parked_kind(), Some(OverlayKind::NoteBrowser));
}
#[test]
fn close_restores_the_parked_overlay_without_a_focus_token() {
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(FakeOverlay(OverlayKind::NoteBrowser)), 1);
host.open_over(Box::new(FakeOverlay(OverlayKind::Dialog)));
assert_eq!(host.close(), None, "the opener is the parked overlay");
assert_eq!(host.active_kind(), Some(OverlayKind::NoteBrowser));
assert_eq!(host.parked_kind(), None);
assert_eq!(host.close(), Some(1), "second close returns the opener");
assert!(!host.is_open());
}
#[test]
fn open_drops_the_parked_overlay() {
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(FakeOverlay(OverlayKind::NoteBrowser)), 1);
host.open_over(Box::new(FakeOverlay(OverlayKind::Dialog)));
host.open(Box::new(FakeOverlay(OverlayKind::SavedSearches)), 2);
assert_eq!(host.parked_kind(), None);
assert_eq!(host.close(), Some(1), "the original opener focus");
assert!(!host.is_open());
}
#[test]
fn close_all_closes_the_whole_stack() {
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(FakeOverlay(OverlayKind::NoteBrowser)), 1);
host.open_over(Box::new(FakeOverlay(OverlayKind::Dialog)));
assert_eq!(host.close_all(), Some(1));
assert!(!host.is_open());
assert_eq!(host.parked_kind(), None);
assert_eq!(host.close_all(), None, "nothing left to close");
}
#[test]
fn render_draws_parked_and_active() {
use ratatui::{Terminal, backend::TestBackend};
use std::sync::atomic::{AtomicUsize, Ordering};
static DRAWN: AtomicUsize = AtomicUsize::new(0);
struct Counting(OverlayKind);
impl Overlay for Counting {
fn kind(&self) -> OverlayKind {
self.0
}
fn handle_input(&mut self, _e: &InputEvent, _tx: &AppTx) -> EventState {
EventState::Consumed
}
fn render(&mut self, _f: &mut Frame, _a: Rect, _t: &Theme) {
DRAWN.fetch_add(1, Ordering::SeqCst);
}
}
let mut host: OverlayHost<u8> = OverlayHost::new();
host.open(Box::new(Counting(OverlayKind::NoteBrowser)), 1);
host.open_over(Box::new(Counting(OverlayKind::Dialog)));
let theme = Theme::gruvbox_dark();
let mut t = Terminal::new(TestBackend::new(40, 10)).unwrap();
t.draw(|f| host.render(f, f.area(), &theme)).unwrap();
assert_eq!(DRAWN.load(Ordering::SeqCst), 2);
}
}