From 042007d9edcd23a5926a327e91b69a2137f74b1c Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 9 Mar 2021 14:19:52 +0100 Subject: [PATCH] Layout View --- src/ui/layout/view.rs | 312 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 293 insertions(+), 19 deletions(-) diff --git a/src/ui/layout/view.rs b/src/ui/layout/view.rs index 629c3a0..c42890f 100644 --- a/src/ui/layout/view.rs +++ b/src/ui/layout/view.rs @@ -24,7 +24,7 @@ */ // imports -use super::{Component, Msg, Props}; +use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder}; // ext use std::collections::HashMap; @@ -32,21 +32,20 @@ use std::collections::HashMap; /// /// View is the wrapper and manager for all the components. /// A View is a container for all the components in a certain layout. -/// It is possible to have ligatures between elements, which causes for example a component a to lose focus in favour of b if a certain key is pressed. /// Each View can have only one focused component. pub struct View { components: HashMap>, // all the components in the view focus: Option, // Current active component - focus_stack: Vec, // Focus stack; used to give focus in case the current element loses focus + focus_stack: Vec, // Focus stack; used to give focus in case the current element loses focus } // -- view impl View { - /// ### new + /// ### init /// - /// Instantiates a new `View` - pub fn new() -> Self { + /// Initialize a new `View` + pub fn init() -> Self { View { components: HashMap::new(), focus: None, @@ -54,15 +53,86 @@ impl View { } } + // -- mount / umount + + /// ### mount + /// + /// Mount a new component in the view + pub fn mount(&mut self, id: &str, component: Box) { + self.components.insert(id.to_string(), component); + } + + /// ### umount + /// + /// Umount a component from the view + pub fn umount(&mut self, id: &str) { + self.components.remove(id); + } + + // -- props + + /// ### get_props + /// + /// Get component properties + pub fn get_props(&self, id: &str) -> Option { + match self.components.get(id) { + None => None, + Some(cmp) => Some(cmp.get_props()), + } + } + + /// update + /// + /// Update component properties + /// Returns `None` if component doesn't exist + pub fn update<'a>(&mut self, id: &'a str, props: Props) -> Option<(&'a str, Msg)> { + match self.components.get_mut(id) { + None => None, + Some(cmp) => Some((id, cmp.update(props))), + } + } + + // -- state + + /// ### get_value + /// + /// Get component value + pub fn get_value(&self, id: &str) -> Option { + match self.components.get(id) { + None => None, + Some(cmp) => Some(cmp.get_value()), + } + } + + // -- events + + /// ### on + /// + /// Handle event for the focused component (if any) + /// Returns `None` if no component is focused + pub fn on(&mut self, ev: InputEvent) -> Option<(&str, Msg)> { + match self.focus.as_ref() { + None => None, + Some(id) => match self.components.get_mut(id) { + None => None, + Some(cmp) => Some((id, cmp.on(ev))), + }, + } + } + // -- private /// ### blur /// - /// Blur selected element and push it into the stack; - /// Last element in stack becomes active - fn blur(&mut self) { + /// Blur selected element AND DON'T PUSH CURRENT ACTIVE ELEMENT INTO THE STACK + /// Last element in stack becomes active and is removed from the stack + pub fn blur(&mut self) { if let Some(component) = self.focus.take() { - // Set last element as active + // Blur component + if let Some(cmp) = self.components.get_mut(component.as_str()) { + cmp.blur(); + } + // Set last element in the stack as active let mut new: Option = None; if let Some(last) = self.focus_stack.last() { // Set focus to last element @@ -73,21 +143,27 @@ impl View { if let Some(new) = new { self.pop_from_stack(new.as_str()); } - // Finally previous active component to stack - self.push_to_stack(&component); } } /// ### active - /// + /// /// Active provided element - fn active(&mut self, component: &str) { - // If there is an element active; call blur - if self.focus.is_some() { - self.blur(); + /// Current active component, if any, GETS PUSHED to the STACK + pub fn active(&mut self, component: &str) { + // Active component if exists + if let Some(cmp) = self.components.get_mut(component) { + // Active component + cmp.active(); + // Put current focus if any, into the stack + if let Some(active_component) = self.focus.take() { + self.push_to_stack(active_component.as_str()); + } + // Give focus to component + self.focus = Some(component.to_string()); + // Remove new component from the stack + self.pop_from_stack(component); } - // Set focus - self.focus = Some(component.to_string()); } /// ### push_to_stack @@ -105,3 +181,201 @@ impl View { self.focus_stack.retain(|c| c.as_str() != name); } } + +#[cfg(test)] +mod tests { + + use super::*; + + use super::super::components::input::Input; + use super::super::components::text::Text; + use super::super::props::{PropValue, TextParts, TextSpan}; + + use crossterm::event::{KeyCode, KeyEvent}; + + #[test] + fn test_ui_layout_view_init() { + let view: View = View::init(); + // Verify view + assert_eq!(view.components.len(), 0); + assert!(view.focus.is_none()); + assert_eq!(view.focus_stack.len(), 0); + } + + #[test] + fn test_ui_layout_view_mount_umount() { + let mut view: View = View::init(); + // Mount component + let input: &str = "INPUT"; + view.mount(input, make_component_input()); + // Verify is mounted + assert!(view.components.get(input).is_some()); + // Mount another + let text: &str = "TEXT"; + view.mount(text, make_component_text()); + assert!(view.components.get(text).is_some()); + assert_eq!(view.components.len(), 2); + // Verify you cannot have duplicates + view.mount(input, make_component_input()); + assert_eq!(view.components.len(), 2); // length should still be 2 + // Umount + view.umount(text); + assert_eq!(view.components.len(), 1); + assert!(view.components.get(text).is_none()); + } + + #[test] + fn test_ui_layout_view_focus() { + let mut view: View = View::init(); + // Prepare ids + let input1: &str = "INPUT_1"; + let input2: &str = "INPUT_2"; + let input3: &str = "INPUT_3"; + let text1: &str = "TEXT_1"; + let text2: &str = "TEXT_2"; + // Mount components + view.mount(input1, make_component_input()); + view.mount(input2, make_component_input()); + view.mount(input3, make_component_input()); + view.mount(text1, make_component_text()); + view.mount(text2, make_component_text()); + // Verify focus + assert!(view.focus.is_none()); + assert_eq!(view.focus_stack.len(), 0); + // Blur when nothing is selected + view.blur(); + assert!(view.focus.is_none()); + assert_eq!(view.focus_stack.len(), 0); + // Active unexisting component + view.active("UNEXISTING-COMPONENT"); + assert!(view.focus.is_none()); + assert_eq!(view.focus_stack.len(), 0); + // Give focus to a component + view.active(input1); + // Check focus + assert_eq!(view.focus.as_ref().unwrap().as_str(), input1); + assert_eq!(view.focus_stack.len(), 0); // NOTE: stack is empty until a focus gets blurred + // Active a new component + view.active(input2); + // Now focus should be on input2, but input 1 should be in the focus stack + assert_eq!(view.focus.as_ref().unwrap().as_str(), input2); + assert_eq!(view.focus_stack.len(), 1); + assert_eq!(view.focus_stack[0].as_str(), input1); + // Active input 3 + view.active(input3); + // now focus should be hold by input3, and stack should have len 2 + assert_eq!(view.focus.as_ref().unwrap().as_str(), input3); + assert_eq!(view.focus_stack.len(), 2); + assert_eq!(view.focus_stack[0].as_str(), input1); + assert_eq!(view.focus_stack[1].as_str(), input2); + // blur + view.blur(); + // Focus should now be hold by input2; input 3 should NOT be in the stack + assert_eq!(view.focus.as_ref().unwrap().as_str(), input2); + assert_eq!(view.focus_stack.len(), 1); + assert_eq!(view.focus_stack[0].as_str(), input1); + // Active twice + view.active(input2); + // Nothing should have changed + assert_eq!(view.focus.as_ref().unwrap().as_str(), input2); + assert_eq!(view.focus_stack.len(), 1); + assert_eq!(view.focus_stack[0].as_str(), input1); + // Blur again; stack should become empty, whether focus should then be hold by input 1 + view.blur(); + assert_eq!(view.focus.as_ref().unwrap().as_str(), input1); + assert_eq!(view.focus_stack.len(), 0); + // Blur again; now everything should be none + view.blur(); + assert!(view.focus.is_none()); + assert_eq!(view.focus_stack.len(), 0); + } + + #[test] + fn test_ui_layout_view_update() { + let mut view: View = View::init(); + // Prepare ids + let text: &str = "TEXT"; + // Mount text + view.mount(text, make_component_text()); + // Get properties and update + let props: Props = view.get_props(text).unwrap().build(); + // Verify bold is false + assert_eq!(props.bold, false); + // Update properties and set bold to true + let mut builder = view.get_props(text).unwrap(); + let (id, msg) = view.update(text, builder.bold().build()).unwrap(); + // Verify return values + assert_eq!(id, text); + assert_eq!(msg, Msg::None); + // Verify bold is now true + let props: Props = view.get_props(text).unwrap().build(); + // Verify bold is false + assert_eq!(props.bold, true); + // Get properties for unexisting component + assert!(view.update("foobar", props).is_none()); + } + + #[test] + fn test_ui_layout_view_on() { + let mut view: View = View::init(); + // Prepare ids + let text: &str = "TEXT"; + let input: &str = "INPUT"; + // Mount + view.mount(text, make_component_text()); + view.mount(input, make_component_input()); + // Verify current value + assert_eq!(view.get_value(text).unwrap(), Payload::None); // Text value is Nothing + assert_eq!( + view.get_value(input).unwrap(), + Payload::Text(String::from("text")) + ); // Defined in `make_component_input` + // Handle events WITHOUT ANY ACTIVE ELEMENT + assert!(view + .on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))) + .is_none()); + // Active input + view.active(input); + // Now handle events on input + // Try char + assert_eq!( + view.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1')))) + .unwrap(), + (input, Msg::None) + ); + // Verify new value + assert_eq!( + view.get_value(input).unwrap(), + Payload::Text(String::from("text1")) + ); + // Verify enter + assert_eq!( + view.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))) + .unwrap(), + (input, Msg::OnSubmit(Payload::Text(String::from("text1")))) + ); + } + + /// ### make_component + /// + /// Make a new component; we'll use Input, which uses a rich set of events + fn make_component_input() -> Box { + Box::new(Input::new( + PropsBuilder::default() + .with_texts(TextParts::new(Some(String::from("mytext")), None)) + .with_value(PropValue::Str(String::from("text"))) + .build(), + )) + } + + fn make_component_text() -> Box { + Box::new(Text::new( + PropsBuilder::default() + .with_texts(TextParts::new( + None, + Some(vec![TextSpan::from("Sample text")]), + )) + .build(), + )) + } +}