use crate::tui::component::{Action, Component}; use crate::tui::editor::Editor; use crate::tui::explorer::Explorer; use ratatui::buffer::Buffer; use ratatui::crossterm::event; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::prelude::{Color, Style, Widget}; use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap}; use ratatui::{DefaultTerminal, symbols}; use std::time::Duration; pub struct App<'a> { explorer: Explorer<'a>, editor: Editor, } impl<'a> App<'a> { pub(crate) fn new(root_path: std::path::PathBuf) -> Self { let mut app = Self { explorer: Explorer::new(&root_path), editor: Editor::new(), }; app.editor .set_contents(&root_path.join("src/tui/app.rs")) .expect("Failed to set editor contents."); app } fn get_event(&mut self) -> Option { if !event::poll(Duration::from_millis(250)).expect("event poll failed") { return None; } event::read().ok() } pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> { loop { // TODO: Handle events based on which component is active. terminal.draw(|f| { f.render_widget(&mut self, f.area()); })?; if let Some(event) = self.get_event() { match self.handle_event(event) { Action::Quit => break, Action::Handled => {} _ => { // panic!("Unhandled event: {:?}", event); } } } } Ok(()) } fn draw_status(&self, area: Rect, buf: &mut Buffer) { // TODO: Status bar should have drop down menus Tabs::new(["File", "Edit", "View", "Help"]) .style(Style::default()) .block(Block::default().borders(Borders::ALL)) .render(area, buf); } fn draw_tabs(&self, area: Rect, buf: &mut Buffer) { // TODO: Tabs should be opened from file explorer Tabs::new(["file.md", "file.cpp"]) .divider(symbols::DOT) .block( Block::default() .borders(Borders::NONE) .padding(Padding::new(0, 0, 0, 0)), ) .highlight_style(Style::default().fg(Color::LightRed)) .render(area, buf); } fn draw_terminal(&self, area: Rect, buf: &mut Buffer) { // TODO: Title should be detected shell name // TODO: Contents should be shell output Paragraph::new("shaun@pc:~/Code/clide$ ") .style(Style::default()) .block( Block::default() .title("Bash") .title_style(Style::default().fg(Color::DarkGray)) .borders(Borders::ALL), ) .wrap(Wrap { trim: false }) .render(area, buf); } } // TODO: Separate complex components into their own widgets. impl<'a> Widget for &mut App<'a> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { let vertical = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // status bar Constraint::Percentage(70), // horizontal layout Constraint::Percentage(30), // terminal ]) .split(area); let horizontal = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Max(30), // File explorer with a max width of 30 characters. Constraint::Fill(1), // Editor fills the remaining space. ]) .split(vertical[1]); let editor_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // Editor tabs. Constraint::Fill(1), // Editor contents. ]) .split(horizontal[1]); self.draw_status(vertical[0], buf); self.draw_terminal(vertical[2], buf); self.explorer.render(horizontal[0], buf); self.draw_tabs(editor_layout[0], buf); self.editor.render(editor_layout[1], buf); } } impl<'a> Component for App<'a> { fn id(&self) -> &str { "app" } /// TODO: Get active widget with some Component trait function helper? /// trait Component { fn get_state() -> ComponentState; } /// if component.get_state() = ComponentState::Active { component.handle_event(); } /// /// App could then provide helpers for altering Component state based on TUI grouping.. /// (such as editor tabs, file explorer, status bars, etc..) fn handle_event(&mut self, event: Event) -> Action { // Handle events in the primary application. if let Some(key_event) = event.as_key_event() { match self.handle_key_events(key_event) { Action::Quit => return Action::Quit, Action::Handled => { dbg!(format!("Handled event: {:?}", self.id())); return Action::Handled; } _ => {} } } self.editor.handle_event(event); // Handle events for all components. // for component in &mut self.components { // dbg!(format!("Handling event: {:?}", component.id())); // // Actions returned here abort the input handling iteration. // match component.handle_event(event.clone()) { // Action::Quit => return Action::Quit, // Action::Handled => return Action::Handled, // _ => continue, // } // } Action::Noop } fn handle_key_events(&mut self, key: KeyEvent) -> Action { match key { KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: _state, } => Action::Quit, key_event => { // Pass the key event to each component that can handle it. self.explorer.handle_key_events(key_event) } } } }