1
//! Modal overlay stack.
2
//!
3
//! The TUI keeps a LIFO stack of modals. The topmost modal owns the
4
//! keyboard focus and is rendered on top of the current tab. Key events
5
//! are first offered to the topmost modal; only if it doesn't consume
6
//! them do they fall through to the tab.
7

            
8
use crate::widgets::Editor;
9

            
10
/// A config-set modal: two input fields (name + value) plus a Save/
11
/// Cancel action. Placeholder for richer forms later.
12
#[derive(Debug)]
13
pub struct ConfigSetModal {
14
    pub name: Editor,
15
    pub value: Editor,
16
    pub focus: ConfigSetField,
17
}
18

            
19
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20
pub enum ConfigSetField {
21
    Name,
22
    Value,
23
}
24

            
25
/// Anything renderable-as-modal. Kept as an enum rather than a trait
26
/// object so modal state stays `Clone`/`Debug` and survives through
27
/// `Stack`'s `Vec`.
28
#[derive(Debug)]
29
pub enum Modal {
30
    ConfigSet(ConfigSetModal),
31
    Help,
32
}
33

            
34
/// Stack of modals. The topmost (last-pushed) modal is the active one.
35
#[derive(Debug, Default)]
36
pub struct Stack {
37
    modals: Vec<Modal>,
38
}
39

            
40
impl Stack {
41
    #[must_use]
42
37
    pub const fn new() -> Self {
43
37
        Self { modals: Vec::new() }
44
37
    }
45

            
46
7
    pub fn push(&mut self, modal: Modal) {
47
7
        self.modals.push(modal);
48
7
    }
49

            
50
3
    pub fn pop(&mut self) -> Option<Modal> {
51
3
        self.modals.pop()
52
3
    }
53

            
54
    #[must_use]
55
21
    pub fn top(&self) -> Option<&Modal> {
56
21
        self.modals.last()
57
21
    }
58

            
59
    #[must_use]
60
2
    pub fn top_mut(&mut self) -> Option<&mut Modal> {
61
2
        self.modals.last_mut()
62
2
    }
63

            
64
    #[must_use]
65
102
    pub fn is_empty(&self) -> bool {
66
102
        self.modals.is_empty()
67
102
    }
68
}
69

            
70
#[cfg(test)]
71
mod tests {
72
    use super::*;
73
    use crate::widgets::EditMode;
74

            
75
2
    fn a_config_set_modal() -> Modal {
76
2
        Modal::ConfigSet(ConfigSetModal {
77
2
            name: Editor::new(EditMode::Emacs),
78
2
            value: Editor::new(EditMode::Emacs),
79
2
            focus: ConfigSetField::Name,
80
2
        })
81
2
    }
82

            
83
    #[test]
84
1
    fn empty_stack_has_no_top() {
85
1
        let stack = Stack::new();
86
1
        assert!(stack.is_empty());
87
1
        assert!(stack.top().is_none());
88
1
    }
89

            
90
    #[test]
91
1
    fn push_then_pop_yields_last_in_first_out() {
92
1
        let mut stack = Stack::new();
93
1
        stack.push(Modal::Help);
94
1
        stack.push(a_config_set_modal());
95
1
        let popped = stack.pop().expect("expected a modal");
96
1
        assert!(matches!(popped, Modal::ConfigSet(_)));
97
1
        assert!(matches!(stack.top(), Some(Modal::Help)));
98
1
        assert!(!stack.is_empty());
99
1
    }
100

            
101
    #[test]
102
1
    fn pop_on_empty_returns_none() {
103
1
        let mut stack = Stack::new();
104
1
        assert!(stack.pop().is_none());
105
1
    }
106

            
107
    #[test]
108
1
    fn top_mut_allows_mutating_focus_field() {
109
1
        let mut stack = Stack::new();
110
1
        stack.push(a_config_set_modal());
111
1
        if let Some(Modal::ConfigSet(m)) = stack.top_mut() {
112
1
            m.focus = ConfigSetField::Value;
113
1
        }
114
1
        match stack.top() {
115
1
            Some(Modal::ConfigSet(m)) => assert_eq!(m.focus, ConfigSetField::Value),
116
            _ => panic!("expected ConfigSet modal"),
117
        }
118
1
    }
119
}