diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs new file mode 100644 index 0000000..3a2955c --- /dev/null +++ b/src/gui/elements/console_card.rs @@ -0,0 +1,147 @@ +use actix_web::web::block; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::gui::{ + elements::elements::{Element, InteractableElement, JoinableElement}, + interaction_result::InteractionResult, + util::borders::draw_block_joins, +}; +use std::any::Any; + +pub struct ConsoleCard { + focused: bool, + pub title: String, + pub content: String, + + borders: Borders, + joins: Borders, +} + +impl ConsoleCard { + pub fn new(title: &str, content: &str) -> Self { + ConsoleCard { + focused: false, + title: title.to_string(), + content: content.to_string(), + borders: Borders::ALL, + joins: Borders::NONE, + } + } +} + +impl Element for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, r: Rect) { + let block = Block::default() + .borders(self.borders) + .title(self.title.clone()) + .title_style(Style::default().fg(Color::White)) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }) + .style(if self.focused { + Style::default().fg(Color::White) + } else { + Style::default() + }); + + let par = Paragraph::new(Line::from(Span::from(self.content.clone()))) + .block(block) + .scroll((0, 0)); + f.render_widget(par, r); + draw_block_joins(f, r, self.borders, self.joins); + } +} + +impl JoinableElement for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn interact(&mut self, key: KeyEvent) -> InteractionResult { + match key.code { + KeyCode::Enter => { + self.content = "".to_string(); + InteractionResult::Handled + } + KeyCode::Backspace => { + self.content.pop(); + InteractionResult::Handled + } + _ => { + if let Some(c) = key.code.as_char() { + self.content.push(c); + InteractionResult::Handled + } else { + InteractionResult::Unhandled + } + } + } + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs index bc1a3ec..c5441f1 100644 --- a/src/gui/elements/elements.rs +++ b/src/gui/elements/elements.rs @@ -1,7 +1,7 @@ use std::any::Any; use crossterm::event::KeyEvent; -use ratatui::{Frame, layout::Rect}; +use ratatui::{Frame, layout::Rect, widgets::Borders}; use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; @@ -11,6 +11,15 @@ pub trait Element: Send + Sync + Any { fn render(&self, f: &mut Frame, r: Rect); } +pub trait JoinableElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn set_borders(&mut self, borders: Borders); + fn set_joins(&mut self, joins: Borders); +} pub trait InfoElement: Send + Sync + Any { fn as_any(&self) -> &dyn Any; diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs index 41dc752..27f6a44 100644 --- a/src/gui/elements/log_card.rs +++ b/src/gui/elements/log_card.rs @@ -1,5 +1,6 @@ -use crate::gui::elements::elements::InteractableElement; +use crate::gui::elements::elements::{InteractableElement, JoinableElement}; use crate::gui::interaction_result::InteractionResult; +use crate::gui::util::borders::draw_block_joins; use crate::{APP_STATE, gui::elements::elements::Element}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ @@ -20,6 +21,9 @@ pub struct UiLogEntry { pub struct LogCard { focused: bool, scroll: u16, + + pub borders: Borders, + pub joins: Borders, } impl LogCard { @@ -27,6 +31,8 @@ impl LogCard { Self { focused: false, scroll: 0, + borders: Borders::ALL, + joins: Borders::NONE, } } } @@ -49,8 +55,12 @@ impl Element for LogCard { .collect(); let block = Block::default() - .title("Logs") - .borders(Borders::ALL) + .title(if self.focused { + "Logs J/K to scroll" + } else { + "Logs" + }) + .borders(self.borders) .border_style(if self.focused { Style::default().fg(Color::Yellow) } else { @@ -63,6 +73,32 @@ impl Element for LogCard { .scroll((self.scroll, 0)); f.render_widget(paragraph, area); + draw_block_joins(f, area, self.borders, self.joins); + } +} +impl JoinableElement for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; } } impl InteractableElement for LogCard { @@ -84,13 +120,13 @@ impl InteractableElement for LogCard { fn interact(&mut self, key: KeyEvent) -> InteractionResult { match key.code { - KeyCode::Up => { + KeyCode::Char('J') | KeyCode::Char('j') => { if self.scroll > 0 { self.scroll -= 1; } InteractionResult::Handled } - KeyCode::Down => { + KeyCode::Char('K') | KeyCode::Char('k') => { self.scroll += 1; InteractionResult::Handled } diff --git a/src/gui/interaction_result.rs b/src/gui/interaction_result.rs index b6fa626..462149a 100644 --- a/src/gui/interaction_result.rs +++ b/src/gui/interaction_result.rs @@ -28,3 +28,22 @@ impl Debug for InteractionResult { } } } + +impl PartialEq for InteractionResult { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + InteractionResult::OpenScreen { screen: _ }, + InteractionResult::OpenScreen { screen: _ }, + ) => true, + ( + InteractionResult::OpenFutureScreen { screen: _ }, + InteractionResult::OpenFutureScreen { screen: _ }, + ) => true, + (InteractionResult::CloseScreen, InteractionResult::CloseScreen) => true, + (InteractionResult::Handled, InteractionResult::Handled) => true, + (InteractionResult::Unhandled, InteractionResult::Unhandled) => true, + _ => false, + } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 0593313..87c8eec 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,4 +1,5 @@ pub mod elements { + pub mod console_card; pub mod elements; pub mod log_card; } @@ -8,6 +9,9 @@ pub mod screens { pub mod terms_checker; pub mod terms_updater; } +pub mod util { + pub mod borders; +} pub mod app_state; pub mod input_handler; pub mod interaction_result; diff --git a/src/gui/screens/main_screen.rs b/src/gui/screens/main_screen.rs index ce33695..acec65d 100644 --- a/src/gui/screens/main_screen.rs +++ b/src/gui/screens/main_screen.rs @@ -1,32 +1,125 @@ use crate::gui::{ elements::{ - elements::{Element, InteractableElement}, + console_card::ConsoleCard, + elements::{Element, InteractableElement, JoinableElement}, log_card::LogCard, }, interaction_result::InteractionResult, - screens::screens::Screen, + screens::screens::{NavDirection, Screen}, ui::UI, }; -use crossterm::event::KeyEvent; + +use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ Frame, layout::{Constraint, Layout, Margin, Rect}, widgets::{Block, Borders}, }; + use std::{any::Any, sync::Arc}; pub struct MainScreen { ui: Arc, - log_card: LogCard, + elements: Vec>, + nav_grid: Vec>>, + selected_coords: (usize, usize), } impl MainScreen { pub async fn new(ui: Arc) -> Self { + let mut elements: Vec> = Vec::new(); + let mut nav_grid: Vec>> = Vec::new(); + let mut log_card = LogCard::new(); - log_card.focus(true); - MainScreen { ui, log_card } + log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); + let mut console_card = ConsoleCard::new("Console", ""); + console_card.set_joins(Borders::TOP); + elements.push(Box::new(log_card)); + elements.push(Box::new(console_card)); + + nav_grid.push(vec![Some(0)]); + nav_grid.push(vec![Some(1)]); + + let mut screen = MainScreen { + ui, + elements, + nav_grid, + selected_coords: (1, 0), + }; + + screen.focus_current(); + screen + } + + fn focus_current(&mut self) { + let (y, x) = self.selected_coords; + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { + if let Some(element) = self.elements.get_mut(*index) { + if element.can_focus() { + element.focus(true); + } + } + } + } + + fn unfocus_current(&mut self, y: usize, x: usize) { + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { + if let Some(element) = self.elements.get_mut(*index) { + element.focus(false); + } + } + } + + fn navigate(&mut self, direction: NavDirection) { + let (mut y, mut x) = self.selected_coords; + + self.unfocus_current(y, x); + + match direction { + NavDirection::Up => { + if y > 0 { + y -= 1; + } + } + NavDirection::Down => { + if y < self.nav_grid.len() - 1 { + y += 1; + } + } + NavDirection::Left => { + if x > 0 { + x -= 1; + } + } + NavDirection::Right => { + if let Some(row) = self.nav_grid.get(y) { + if x < row.len() - 1 { + x += 1; + } + } + } + _ => {} + } + + // Clamp X to row length + if let Some(row) = self.nav_grid.get(y) { + if x >= row.len() { + x = row.len() - 1; + } + } + + if self + .nav_grid + .get(y) + .and_then(|r| r.get(x)) + .map_or(false, |e| e.is_some()) + { + self.selected_coords = (y, x); + self.focus_current(); + } } } + impl Screen for MainScreen { fn as_any(&self) -> &dyn Any { self @@ -42,7 +135,6 @@ impl Screen for MainScreen { fn render(&self, f: &mut Frame, rect: Rect) { let main_block = Block::default().title("Main").borders(Borders::ALL); - f.render_widget(main_block, rect); let inner = rect.inner(Margin { @@ -50,16 +142,33 @@ impl Screen for MainScreen { horizontal: 1, }); - let chunks = Layout::vertical([Constraint::Min(0)]).split(inner); + let chunks = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(inner); - self.log_card.render(f, chunks[0]); + if let Some(Some(index)) = self.nav_grid.get(0).and_then(|r| r.get(0)) { + self.elements[*index].as_element().render(f, chunks[0]); + } + + if let Some(Some(index)) = self.nav_grid.get(1).and_then(|r| r.get(0)) { + self.elements[*index].as_element().render(f, chunks[1]); + } } fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - if self.log_card.is_focused() { - return self.log_card.interact(event); + match event.code { + KeyCode::Up => self.navigate(NavDirection::Up), + KeyCode::Down => self.navigate(NavDirection::Down), + KeyCode::Left => self.navigate(NavDirection::Left), + KeyCode::Right => self.navigate(NavDirection::Right), + _ => { + let (y, x) = self.selected_coords; + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) { + if let Some(el) = self.elements.get_mut(*index) { + return el.interact(event); + } + } + } } - InteractionResult::Unhandled + InteractionResult::Handled } } diff --git a/src/gui/util/borders.rs b/src/gui/util/borders.rs new file mode 100644 index 0000000..34460bc --- /dev/null +++ b/src/gui/util/borders.rs @@ -0,0 +1,63 @@ +use ratatui::layout::Rect; +use ratatui::prelude::*; +use ratatui::style::Style; +use ratatui::widgets::Borders; + +fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) { + frame + .buffer_mut() + .set_string(x, y, c.to_string(), Style::default()); +} + +pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) { + let x0 = area.x; + let y0 = area.y; + let x1 = area.x + area.width - 1; + let y1 = area.y + area.height - 1; + + if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) { + let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┬', + (false, false) => '┌', + }; + set_join_char(frame, x0, y0, top_left); + } + + if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) { + let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┬', + (false, false) => '┐', + }; + set_join_char(frame, x1, y0, top_right); + } + + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) { + let bottom_left = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::LEFT), + ) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┴', + (false, false) => '└', + }; + set_join_char(frame, x0, y1, bottom_left); + } + + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) { + let bottom_right = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::RIGHT), + ) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┴', + (false, false) => '┘', + }; + set_join_char(frame, x1, y1, bottom_right); + } +} diff --git a/src/main.rs b/src/main.rs index 0ac48dd..b9b4ffd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,7 @@ async fn main() { if ACTIVE_TASKS.lock().unwrap().is_empty() { break; } - sleep(Duration::from_secs(1)).await; + sleep(Duration::from_millis(100)).await; } println!( "Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!" diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index 8d90920..f7b0c99 100644 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -151,6 +151,9 @@ impl OmikronConnection { let sel_arc_clone = self.clone(); tokio::spawn(async move { loop { + if *SHUTDOWN.read().await { + break; + } if !sel_arc_clone.is_connected().await { break; }