//! ## Model //! //! app model use std::time::{Duration, SystemTime}; use tuirealm::event::NoUserEvent; use tuirealm::props::{Alignment, Color, TextModifiers}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter, TerminalBridge}; use tuirealm::{ Application, AttrValue, Attribute, EventListenerCfg, Sub, SubClause, SubEventClause, Update, }; use super::components::{Clock, DigitCounter, Label, LetterCounter}; use super::{Id, Msg}; pub struct Model where T: TerminalAdapter, { /// Application pub app: Application, /// Indicates that the application must quit pub quit: bool, /// Tells whether to redraw interface pub redraw: bool, /// Used to draw to terminal pub terminal: TerminalBridge, } impl Default for Model { fn default() -> Self { Self { app: Self::init_app(), quit: false, redraw: true, terminal: TerminalBridge::init_crossterm().expect("Cannot initialize terminal"), } } } impl Model where T: TerminalAdapter, { pub fn view(&mut self) { assert!(self .terminal .draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .margin(1) .constraints( [ Constraint::Length(3), // Clock Constraint::Length(3), // Letter Counter Constraint::Length(3), // Digit Counter Constraint::Length(1), // Label ] .as_ref(), ) .split(f.area()); self.app.view(&Id::Clock, f, chunks[0]); self.app.view(&Id::LetterCounter, f, chunks[1]); self.app.view(&Id::DigitCounter, f, chunks[2]); self.app.view(&Id::Label, f, chunks[3]); }) .is_ok()); } fn init_app() -> Application { // Setup application // NOTE: NoUserEvent is a shorthand to tell tui-realm we're not going to use any custom user event // NOTE: the event listener is configured to use the default crossterm input listener and to raise a Tick event each second // which we will use to update the clock let mut app: Application = Application::init( EventListenerCfg::default() .crossterm_input_listener(Duration::from_millis(20), 3) .poll_timeout(Duration::from_millis(10)) .tick_interval(Duration::from_secs(1)), ); // Mount components assert!(app .mount( Id::Label, Box::new( Label::default() .text("Waiting for a Msg...") .alignment(Alignment::Left) .background(Color::Reset) .foreground(Color::LightYellow) .modifiers(TextModifiers::BOLD), ), Vec::default(), ) .is_ok()); // Mount clock, subscribe to tick assert!(app .mount( Id::Clock, Box::new( Clock::new(SystemTime::now()) .alignment(Alignment::Center) .background(Color::Reset) .foreground(Color::Cyan) .modifiers(TextModifiers::BOLD) ), vec![Sub::new(SubEventClause::Tick, SubClause::Always)] ) .is_ok()); // Mount counters assert!(app .mount( Id::LetterCounter, Box::new(LetterCounter::new(0)), Vec::new() ) .is_ok()); assert!(app .mount( Id::DigitCounter, Box::new(DigitCounter::new(5)), Vec::default() ) .is_ok()); // Active letter counter assert!(app.active(&Id::LetterCounter).is_ok()); app } } // Let's implement Update for model impl Update for Model where T: TerminalAdapter, { fn update(&mut self, msg: Option) -> Option { if let Some(msg) = msg { // Set redraw self.redraw = true; // Match message match msg { Msg::AppClose => { self.quit = true; // Terminate None } Msg::Clock => None, Msg::DigitCounterBlur => { // Give focus to letter counter assert!(self.app.active(&Id::LetterCounter).is_ok()); None } Msg::DigitCounterChanged(v) => { // Update label assert!(self .app .attr( &Id::Label, Attribute::Text, AttrValue::String(format!("DigitCounter has now value: {}", v)) ) .is_ok()); None } Msg::LetterCounterBlur => { // Give focus to digit counter assert!(self.app.active(&Id::DigitCounter).is_ok()); None } Msg::LetterCounterChanged(v) => { // Update label assert!(self .app .attr( &Id::Label, Attribute::Text, AttrValue::String(format!("LetterCounter has now value: {}", v)) ) .is_ok()); None } } } else { None } } }