Lines
98.11 %
Functions
64.71 %
Branches
100 %
//! Modal overlay stack.
//!
//! The TUI keeps a LIFO stack of modals. The topmost modal owns the
//! keyboard focus and is rendered on top of the current tab. Key events
//! are first offered to the topmost modal; only if it doesn't consume
//! them do they fall through to the tab.
use crate::widgets::Editor;
/// A config-set modal: two input fields (name + value) plus a Save/
/// Cancel action. Placeholder for richer forms later.
#[derive(Debug)]
pub struct ConfigSetModal {
pub name: Editor,
pub value: Editor,
pub focus: ConfigSetField,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSetField {
Name,
Value,
/// Anything renderable-as-modal. Kept as an enum rather than a trait
/// object so modal state stays `Clone`/`Debug` and survives through
/// `Stack`'s `Vec`.
pub enum Modal {
ConfigSet(ConfigSetModal),
Help,
/// Stack of modals. The topmost (last-pushed) modal is the active one.
#[derive(Debug, Default)]
pub struct Stack {
modals: Vec<Modal>,
impl Stack {
#[must_use]
pub const fn new() -> Self {
Self { modals: Vec::new() }
pub fn push(&mut self, modal: Modal) {
self.modals.push(modal);
pub fn pop(&mut self) -> Option<Modal> {
self.modals.pop()
pub fn top(&self) -> Option<&Modal> {
self.modals.last()
pub fn top_mut(&mut self) -> Option<&mut Modal> {
self.modals.last_mut()
pub fn is_empty(&self) -> bool {
self.modals.is_empty()
#[cfg(test)]
mod tests {
use super::*;
use crate::widgets::EditMode;
fn a_config_set_modal() -> Modal {
Modal::ConfigSet(ConfigSetModal {
name: Editor::new(EditMode::Emacs),
value: Editor::new(EditMode::Emacs),
focus: ConfigSetField::Name,
})
#[test]
fn empty_stack_has_no_top() {
let stack = Stack::new();
assert!(stack.is_empty());
assert!(stack.top().is_none());
fn push_then_pop_yields_last_in_first_out() {
let mut stack = Stack::new();
stack.push(Modal::Help);
stack.push(a_config_set_modal());
let popped = stack.pop().expect("expected a modal");
assert!(matches!(popped, Modal::ConfigSet(_)));
assert!(matches!(stack.top(), Some(Modal::Help)));
assert!(!stack.is_empty());
fn pop_on_empty_returns_none() {
assert!(stack.pop().is_none());
fn top_mut_allows_mutating_focus_field() {
if let Some(Modal::ConfigSet(m)) = stack.top_mut() {
m.focus = ConfigSetField::Value;
match stack.top() {
Some(Modal::ConfigSet(m)) => assert_eq!(m.focus, ConfigSetField::Value),
_ => panic!("expected ConfigSet modal"),