diff --git a/src/app.rs b/src/app.rs index b0e2f47..9553fff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,7 +12,10 @@ use tracing::{debug, info}; use crate::{ action::Action, - components::{fps::FpsCounter, title::Title, Component}, + components::{ + footer::Footer, fps::FpsCounter, left::Left, popup::Popup, right::Right, title::Title, + Component, + }, config::Config, tui::{Event, Tui}, }; @@ -42,7 +45,14 @@ impl App { Ok(Self { tick_rate, frame_rate, - components: vec![Box::new(Title::new()), Box::new(FpsCounter::default())], + components: vec![ + Box::new(Title::new()), + Box::new(FpsCounter::default()), + Box::new(Left::new()), + Box::new(Right::new()), + Box::new(Footer::new()), + Box::new(Popup::new()), + ], should_quit: false, should_suspend: false, config: Config::new()?, @@ -169,11 +179,11 @@ impl App { fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { - if let [header, _body, _footer, _left, _right, _popup] = get_chunks(frame.area())[..] { + if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] { let component_areas = vec![ header, // Title Bar header, // FPS Counter - // left, right, footer, popup, + left, right, footer, popup, ]; for (component, area) in zip(self.components.iter_mut(), component_areas) { if let Err(err) = component.draw(frame, area) { diff --git a/src/components.rs b/src/components.rs index 1852794..a4d2c68 100644 --- a/src/components.rs +++ b/src/components.rs @@ -8,7 +8,11 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{action::Action, config::Config, tui::Event}; +pub mod footer; pub mod fps; +pub mod left; +pub mod popup; +pub mod right; pub mod title; /// `Component` is a trait that represents a visual and interactive element of the user interface. diff --git a/src/components/footer.rs b/src/components/footer.rs new file mode 100644 index 0000000..b202c5e --- /dev/null +++ b/src/components/footer.rs @@ -0,0 +1,59 @@ +use color_eyre::Result; +use ratatui::{prelude::*, widgets::*}; +use tokio::sync::mpsc::UnboundedSender; + +use super::Component; +use crate::{action::Action, config::Config}; + +#[derive(Default)] +pub struct Footer { + command_tx: Option>, + config: Config, + show_install_driver: bool, +} + +impl Footer { + pub fn new() -> Self { + Self::default() + } +} + +impl Component for Footer { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Tick => { + // add any logic here that should run on every tick + } + Action::Render => { + // add any logic here that should run on every render + } + _ => {} + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let footer_text = Span::styled( + if self.show_install_driver { + "(Enter) to select / (b) to go back / (i) to install driver / (q) to quit" + } else { + "(Enter) to select / (b) to go back / (q) to quit" + }, + Style::default().fg(Color::DarkGray), + ); + let footer = Paragraph::new(Line::from(footer_text).centered()) + .block(Block::default().borders(Borders::ALL)); + frame.render_widget(footer, area); + Ok(()) + } +} diff --git a/src/components/left.rs b/src/components/left.rs new file mode 100644 index 0000000..4d15747 --- /dev/null +++ b/src/components/left.rs @@ -0,0 +1,74 @@ +use color_eyre::Result; +use crossterm::event::KeyEvent; +use ratatui::{prelude::*, widgets::*}; +use tokio::sync::mpsc::UnboundedSender; + +use super::Component; +use crate::{action::Action, config::Config}; + +#[derive(Default)] +pub struct Left { + command_tx: Option>, + config: Config, +} + +impl Left { + pub fn new() -> Self { + Self::default() + } +} + +impl Component for Left { + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + // TODO + let _ = key; // to appease clippy + Ok(None) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Tick => { + // add any logic here that should run on every tick + } + Action::Render => { + // add any logic here that should run on every render + } + _ => {} + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [title_area, body_area] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(1)]) + .areas(area); + + // Title + let title_text = String::from("Hello world!"); + let title = Paragraph::new(Line::from(title_text).centered()) + .block(Block::default().borders(Borders::NONE)); + frame.render_widget(title, title_area); + + // Body + let paragraph = Paragraph::new("Body text...").block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ); + frame.render_widget(paragraph, body_area); + + // Done + Ok(()) + } +} diff --git a/src/components/popup.rs b/src/components/popup.rs new file mode 100644 index 0000000..a0fc22a --- /dev/null +++ b/src/components/popup.rs @@ -0,0 +1,56 @@ +use color_eyre::Result; +use ratatui::{prelude::*, widgets::*}; +use tokio::sync::mpsc::UnboundedSender; + +use super::Component; +use crate::{action::Action, config::Config}; + +#[derive(Default)] +pub struct Popup { + command_tx: Option>, + config: Config, +} + +impl Popup { + pub fn new() -> Self { + Self::default() + } +} + +impl Component for Popup { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Tick => { + // add any logic here that should run on every tick + } + Action::Render => { + // add any logic here that should run on every render + } + _ => {} + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let popup_block = Block::default() + .borders(Borders::ALL) + .style(Style::default().cyan().bold()); + let popup = Paragraph::new(vec![Line::from(Span::raw("Popup text..."))]) + .block(popup_block) + .centered() + .wrap(Wrap { trim: false }); + frame.render_widget(Clear, area); + frame.render_widget(popup, area); + Ok(()) + } +} diff --git a/src/components/right.rs b/src/components/right.rs new file mode 100644 index 0000000..92cc2d7 --- /dev/null +++ b/src/components/right.rs @@ -0,0 +1,74 @@ +use color_eyre::Result; +use crossterm::event::KeyEvent; +use ratatui::{prelude::*, widgets::*}; +use tokio::sync::mpsc::UnboundedSender; + +use super::Component; +use crate::{action::Action, config::Config}; + +#[derive(Default)] +pub struct Right { + command_tx: Option>, + config: Config, +} + +impl Right { + pub fn new() -> Self { + Self::default() + } +} + +impl Component for Right { + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + // TODO ?? + let _ = key; // to appease clippy + Ok(None) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Tick => { + // add any logic here that should run on every tick + } + Action::Render => { + // add any logic here that should run on every render + } + _ => {} + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [title_area, body_area] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(1)]) + .areas(area); + + // Title + let title_text = String::from("Info?"); + let title = Paragraph::new(Line::from(title_text).centered()) + .block(Block::default().borders(Borders::NONE)); + frame.render_widget(title, title_area); + + // Body + let paragraph = Paragraph::new("Some info...").block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ); + frame.render_widget(paragraph, body_area); + + // Done + Ok(()) + } +}