use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use ftui_render::frame::Frame;
use serde::{Deserialize, Serialize};
use crate::input::InputEvent;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ScreenId(pub String);
impl ScreenId {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl fmt::Display for ScreenId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct ScreenContext {
pub active_screen: ScreenId,
pub terminal_width: u16,
pub terminal_height: u16,
pub focused: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeybindingHint {
pub key: &'static str,
pub description: &'static str,
}
pub trait Screen: Send {
fn id(&self) -> &ScreenId;
fn title(&self) -> &str;
fn render(&self, frame: &mut Frame, ctx: &ScreenContext);
fn handle_input(&mut self, event: &InputEvent, ctx: &ScreenContext) -> ScreenAction;
fn on_focus(&mut self) {}
fn on_blur(&mut self) {}
fn semantic_role(&self) -> &'static str {
"region"
}
fn keybindings(&self) -> &'static [KeybindingHint] {
&[]
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScreenAction {
Consumed,
Ignored,
Navigate(ScreenId),
OpenOverlay(String),
Quit,
}
pub struct ScreenRegistry {
screens: HashMap<ScreenId, Box<dyn Screen>>,
order: Vec<ScreenId>,
}
impl ScreenRegistry {
#[must_use]
pub fn new() -> Self {
Self {
screens: HashMap::new(),
order: Vec::new(),
}
}
pub fn register(&mut self, screen: Box<dyn Screen>) {
let id = screen.id().clone();
self.order.push(id.clone());
self.screens.insert(id, screen);
}
#[must_use]
pub fn get(&self, id: &ScreenId) -> Option<&dyn Screen> {
self.screens.get(id).map(AsRef::as_ref)
}
pub fn get_mut(&mut self, id: &ScreenId) -> Option<&mut Box<dyn Screen>> {
self.screens.get_mut(id)
}
#[must_use]
pub fn screen_ids(&self) -> &[ScreenId] {
&self.order
}
#[must_use]
pub fn next_screen(&self, current: &ScreenId) -> Option<&ScreenId> {
let pos = self.order.iter().position(|id| id == current)?;
let next = (pos + 1) % self.order.len();
self.order.get(next)
}
#[must_use]
pub fn prev_screen(&self, current: &ScreenId) -> Option<&ScreenId> {
let pos = self.order.iter().position(|id| id == current)?;
let prev = if pos == 0 {
self.order.len() - 1
} else {
pos - 1
};
self.order.get(prev)
}
#[must_use]
pub fn len(&self) -> usize {
self.screens.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.screens.is_empty()
}
}
impl Default for ScreenRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use ftui_render::frame::Frame;
use super::*;
struct TestScreen {
id: ScreenId,
title: String,
}
impl TestScreen {
fn new(id: &str, title: &str) -> Self {
Self {
id: ScreenId::new(id),
title: title.to_string(),
}
}
}
impl Screen for TestScreen {
fn id(&self) -> &ScreenId {
&self.id
}
fn title(&self) -> &str {
&self.title
}
fn render(&self, _frame: &mut Frame, _ctx: &ScreenContext) {}
fn handle_input(&mut self, _event: &InputEvent, _ctx: &ScreenContext) -> ScreenAction {
ScreenAction::Ignored
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[test]
fn screen_id_display() {
let id = ScreenId::new("fsfs.search");
assert_eq!(id.to_string(), "fsfs.search");
}
#[test]
fn screen_id_serde_roundtrip() {
let id = ScreenId::new("ops.fleet");
let json = serde_json::to_string(&id).unwrap();
let decoded: ScreenId = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, id);
}
#[test]
fn registry_register_and_lookup() {
let mut reg = ScreenRegistry::new();
reg.register(Box::new(TestScreen::new("a", "Screen A")));
reg.register(Box::new(TestScreen::new("b", "Screen B")));
assert_eq!(reg.len(), 2);
assert!(!reg.is_empty());
assert_eq!(reg.get(&ScreenId::new("a")).unwrap().title(), "Screen A");
assert_eq!(reg.get(&ScreenId::new("b")).unwrap().title(), "Screen B");
assert!(reg.get(&ScreenId::new("c")).is_none());
}
#[test]
fn registry_screen_ids_preserves_order() {
let mut reg = ScreenRegistry::new();
reg.register(Box::new(TestScreen::new("x", "X")));
reg.register(Box::new(TestScreen::new("y", "Y")));
reg.register(Box::new(TestScreen::new("z", "Z")));
let ids: Vec<&str> = reg.screen_ids().iter().map(|id| id.0.as_str()).collect();
assert_eq!(ids, vec!["x", "y", "z"]);
}
#[test]
fn registry_next_wraps_around() {
let mut reg = ScreenRegistry::new();
reg.register(Box::new(TestScreen::new("a", "A")));
reg.register(Box::new(TestScreen::new("b", "B")));
reg.register(Box::new(TestScreen::new("c", "C")));
let next = reg.next_screen(&ScreenId::new("c")).unwrap();
assert_eq!(next, &ScreenId::new("a"));
}
#[test]
fn registry_prev_wraps_around() {
let mut reg = ScreenRegistry::new();
reg.register(Box::new(TestScreen::new("a", "A")));
reg.register(Box::new(TestScreen::new("b", "B")));
let prev = reg.prev_screen(&ScreenId::new("a")).unwrap();
assert_eq!(prev, &ScreenId::new("b"));
}
#[test]
fn empty_registry() {
let reg = ScreenRegistry::new();
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
}
}