use crate::tui::layout::Rect;
use crate::{AttrValue, Attribute, Component, Event, Frame, Injector, State};
use std::collections::HashMap;
use std::hash::Hash;
use thiserror::Error;
pub(crate) type WrappedComponent<Msg, UserEvent> = Box<dyn Component<Msg, UserEvent>>;
pub type ViewResult<T> = Result<T, ViewError>;
#[derive(Debug, Error)]
pub enum ViewError {
#[error("component already mounted")]
ComponentAlreadyMounted,
#[error("component not found")]
ComponentNotFound,
#[error("there's no component to blur")]
NoComponentToBlur,
}
pub struct View<ComponentId, Msg, UserEvent>
where
ComponentId: Eq + PartialEq + Clone + Hash,
Msg: PartialEq,
UserEvent: Eq + PartialEq + Clone + PartialOrd,
{
components: HashMap<ComponentId, WrappedComponent<Msg, UserEvent>>,
focus: Option<ComponentId>,
focus_stack: Vec<ComponentId>,
injectors: Vec<Box<dyn Injector<ComponentId>>>,
}
impl<K, Msg, UserEvent> Default for View<K, Msg, UserEvent>
where
K: Eq + PartialEq + Clone + Hash,
Msg: PartialEq,
UserEvent: Eq + PartialEq + Clone + PartialOrd,
{
fn default() -> Self {
Self {
components: HashMap::new(),
focus: None,
focus_stack: Vec::new(),
injectors: Vec::new(),
}
}
}
impl<K, Msg, UserEvent> View<K, Msg, UserEvent>
where
K: Eq + PartialEq + Clone + Hash,
Msg: PartialEq,
UserEvent: Eq + PartialEq + Clone + PartialOrd,
{
pub fn mount(&mut self, id: K, component: WrappedComponent<Msg, UserEvent>) -> ViewResult<()> {
if self.mounted(&id) {
Err(ViewError::ComponentAlreadyMounted)
} else {
self.components.insert(id.clone(), component);
self.inject(&id)
}
}
pub fn umount(&mut self, id: &K) -> ViewResult<()> {
if !self.mounted(id) {
return Err(ViewError::ComponentNotFound);
}
if self.has_focus(id) {
let _ = self.blur();
}
self.pop_from_stack(id);
self.components.remove(id);
Ok(())
}
pub fn remount(
&mut self,
id: K,
component: WrappedComponent<Msg, UserEvent>,
) -> ViewResult<()> {
let had_focus = self.has_focus(&id);
if self.mounted(&id) {
self.components.remove(&id);
}
self.components.insert(id.clone(), component);
self.inject(&id)?;
if had_focus {
self.active(&id)
} else {
Ok(())
}
}
pub fn umount_all(&mut self) {
self.components.clear();
self.focus_stack.clear();
self.focus = None;
}
pub fn mounted(&self, id: &K) -> bool {
self.components.contains_key(id)
}
pub(crate) fn focus(&self) -> Option<&K> {
self.focus.as_ref()
}
pub fn view(&mut self, id: &K, f: &mut Frame, area: Rect) {
if let Some(c) = self.components.get_mut(id) {
c.view(f, area);
}
}
pub(crate) fn forward(&mut self, id: &K, event: Event<UserEvent>) -> ViewResult<Option<Msg>> {
match self.components.get_mut(id) {
None => Err(ViewError::ComponentNotFound),
Some(c) => Ok(c.on(event)),
}
}
pub fn query(&self, id: &K, query: Attribute) -> ViewResult<Option<AttrValue>> {
match self.components.get(id) {
None => Err(ViewError::ComponentNotFound),
Some(c) => Ok(c.query(query)),
}
}
pub fn attr(&mut self, id: &K, attr: Attribute, value: AttrValue) -> ViewResult<()> {
if let Some(c) = self.components.get_mut(id) {
c.attr(attr, value);
Ok(())
} else {
Err(ViewError::ComponentNotFound)
}
}
pub fn state(&self, id: &K) -> ViewResult<State> {
self.components
.get(id)
.map(|c| c.state())
.ok_or(ViewError::ComponentNotFound)
}
pub fn active(&mut self, id: &K) -> ViewResult<()> {
self.set_focus(id, true)?;
self.change_focus(id);
Ok(())
}
pub fn blur(&mut self) -> ViewResult<()> {
if let Some(id) = self.focus.take() {
self.set_focus(&id, false)?;
self.focus_to_last();
Ok(())
} else {
Err(ViewError::NoComponentToBlur)
}
}
pub fn add_injector(&mut self, injector: Box<dyn Injector<K>>) {
self.injectors.push(injector);
}
fn push_to_stack(&mut self, id: K) {
self.pop_from_stack(&id);
self.focus_stack.push(id);
}
fn pop_from_stack(&mut self, id: &K) {
self.focus_stack.retain(|x| x != id);
}
pub(crate) fn has_focus(&self, who: &K) -> bool {
match self.focus.as_ref() {
None => false,
Some(id) => who == id,
}
}
fn change_focus(&mut self, new_focus: &K) {
if let Some(focus) = self.focus.take() {
let _ = self.set_focus(&focus, false);
self.push_to_stack(focus);
}
self.pop_from_stack(new_focus);
let key = self.components.keys().find(|x| *x == new_focus).unwrap();
self.focus = Some(key.clone());
}
fn focus_to_last(&mut self) {
if let Some(focus) = self.take_last_from_stack() {
let _ = self.active(&focus);
}
}
fn take_last_from_stack(&mut self) -> Option<K> {
self.focus_stack.pop()
}
fn set_focus(&mut self, id: &K, value: bool) -> ViewResult<()> {
if let Some(c) = self.components.get_mut(id) {
c.attr(Attribute::Focus, AttrValue::Flag(value));
Ok(())
} else {
Err(ViewError::ComponentNotFound)
}
}
fn inject(&mut self, id: &K) -> ViewResult<()> {
for (attr, value) in self.properties_to_inject(id) {
if let Err(err) = self.attr(id, attr, value) {
return Err(err);
}
}
Ok(())
}
fn properties_to_inject(&self, id: &K) -> Vec<(Attribute, AttrValue)> {
self.injectors.iter().flat_map(|x| x.inject(id)).collect()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
event::{Key, KeyEvent},
mock::{MockBarInput, MockComponentId, MockEvent, MockFooInput, MockInjector, MockMsg},
StateValue,
};
use pretty_assertions::assert_eq;
#[test]
fn default_view_should_be_empty() {
let view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view.components.is_empty());
assert_eq!(view.focus, None);
assert!(view.focus_stack.is_empty());
}
#[test]
fn view_should_mount_and_umount_components() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert_eq!(view.components.len(), 1);
assert!(view.mounted(&MockComponentId::InputFoo));
assert_eq!(view.mounted(&MockComponentId::InputBar), false);
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_ok());
assert_eq!(view.components.len(), 2);
assert!(view.mounted(&MockComponentId::InputBar));
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_err());
assert_eq!(view.components.len(), 2);
assert!(view.mounted(&MockComponentId::InputBar));
assert!(view.umount(&MockComponentId::InputFoo).is_ok());
assert_eq!(view.components.len(), 1);
assert_eq!(view.mounted(&MockComponentId::InputFoo), false);
assert_eq!(view.mounted(&MockComponentId::InputBar), true);
assert!(view.umount(&MockComponentId::InputBar).is_ok());
assert_eq!(view.components.len(), 0);
assert_eq!(view.mounted(&MockComponentId::InputBar), false);
assert!(view.umount(&MockComponentId::InputBar).is_err());
}
#[test]
fn view_should_remount_component_without_losing_focus_stack() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert!(view.active(&MockComponentId::InputFoo).is_ok());
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_ok());
assert!(view.active(&MockComponentId::InputBar).is_ok());
assert!(view
.remount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert!(view.blur().is_ok());
assert!(view.has_focus(&MockComponentId::InputFoo));
}
#[test]
fn view_should_umount_all() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert_eq!(view.components.len(), 1);
assert!(view.mounted(&MockComponentId::InputFoo));
assert_eq!(view.mounted(&MockComponentId::InputBar), false);
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_ok());
assert_eq!(view.components.len(), 2);
assert!(view.mounted(&MockComponentId::InputBar));
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_err());
assert_eq!(view.components.len(), 2);
assert!(view.active(&MockComponentId::InputFoo).is_ok());
assert!(view.active(&MockComponentId::InputBar).is_ok());
view.umount_all();
assert!(view.components.is_empty());
assert!(view.focus_stack.is_empty());
assert!(view.focus.is_none());
}
#[test]
fn view_should_compile_with_dynamic_names() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
let names: Vec<MockComponentId> = (0..10)
.map(|x| MockComponentId::Dyn(format!("INPUT_{}", x)))
.collect();
names.iter().for_each(|x| {
assert!(view
.mount(x.clone(), Box::new(MockBarInput::default()))
.is_ok());
});
assert_eq!(view.components.len(), 10);
names.iter().for_each(|x| assert!(view.mounted(x)));
}
#[test]
fn view_should_handle_focus() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_ok());
assert!(view
.mount(
MockComponentId::InputOmar,
Box::new(MockBarInput::default())
)
.is_ok());
assert!(view.active(&MockComponentId::InputFoo).is_ok());
assert_eq!(view.focus(), Some(&MockComponentId::InputFoo));
assert!(view.has_focus(&MockComponentId::InputFoo));
assert_eq!(
view.query(&MockComponentId::InputFoo, Attribute::Focus)
.ok()
.unwrap()
.unwrap(),
AttrValue::Flag(true)
);
assert_eq!(view.focus.to_owned().unwrap(), MockComponentId::InputFoo);
assert!(view.focus_stack.is_empty());
assert!(view.active(&MockComponentId::InputBar).is_ok());
assert_eq!(
view.query(&MockComponentId::InputBar, Attribute::Focus)
.ok()
.unwrap()
.unwrap(),
AttrValue::Flag(true)
);
assert_eq!(
view.query(&MockComponentId::InputFoo, Attribute::Focus)
.ok()
.unwrap()
.unwrap(),
AttrValue::Flag(false)
);
assert!(view.has_focus(&MockComponentId::InputBar));
assert_eq!(view.focus_stack.len(), 1);
assert!(view.active(&MockComponentId::InputOmar).is_ok());
assert!(view.has_focus(&MockComponentId::InputOmar));
assert_eq!(view.focus_stack.len(), 2);
assert!(view.active(&MockComponentId::InputFoo).is_ok());
assert!(view.has_focus(&MockComponentId::InputFoo));
assert_eq!(view.focus_stack.len(), 2);
assert!(view.umount(&MockComponentId::InputFoo).is_ok());
assert!(view.has_focus(&MockComponentId::InputOmar));
assert_eq!(view.focus_stack.len(), 1);
assert!(view.umount(&MockComponentId::InputBar).is_ok());
assert!(view.active(&MockComponentId::InputBar).is_err());
assert!(view.has_focus(&MockComponentId::InputOmar));
assert_eq!(view.focus_stack.len(), 0);
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_ok());
assert!(view.active(&MockComponentId::InputBar).is_ok());
assert!(view.blur().is_ok());
assert!(view.has_focus(&MockComponentId::InputOmar));
assert_eq!(view.focus_stack.len(), 0);
assert!(view.mounted(&MockComponentId::InputBar));
assert!(view.blur().is_ok());
assert!(view.blur().is_err());
}
#[test]
fn view_should_forward_events() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
let ev: Event<MockEvent> = Event::Keyboard(KeyEvent::from(Key::Char('a')));
assert_eq!(
view.forward(&MockComponentId::InputFoo, ev)
.ok()
.unwrap()
.unwrap(),
MockMsg::FooInputChanged(String::from("a"))
);
assert!(view
.forward(&MockComponentId::InputBar, Event::Tick)
.is_err());
}
#[test]
fn view_should_read_and_write_attributes() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert_eq!(
view.query(&MockComponentId::InputFoo, Attribute::Focus)
.ok()
.unwrap(),
None
);
assert!(view
.query(&MockComponentId::InputBar, Attribute::Focus)
.is_err());
assert!(view
.attr(
&MockComponentId::InputFoo,
Attribute::Focus,
AttrValue::Flag(true)
)
.is_ok());
assert_eq!(
view.query(&MockComponentId::InputFoo, Attribute::Focus)
.ok()
.unwrap(),
Some(AttrValue::Flag(true))
);
}
#[test]
fn view_should_read_state() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
assert!(view
.mount(MockComponentId::InputFoo, Box::new(MockFooInput::default()))
.is_ok());
assert_eq!(
view.state(&MockComponentId::InputFoo).unwrap(),
State::One(StateValue::String(String::from("")))
);
assert!(view.state(&MockComponentId::InputBar).is_err());
}
#[test]
fn view_should_inject_properties() {
let mut view: View<MockComponentId, MockMsg, MockEvent> = View::default();
view.add_injector(Box::new(MockInjector::default()));
assert!(view
.mount(MockComponentId::InputBar, Box::new(MockBarInput::default()))
.is_ok());
assert_eq!(
view.query(&MockComponentId::InputBar, Attribute::Text)
.ok()
.unwrap()
.unwrap(),
AttrValue::String(String::from("hello, world!"))
);
}
}