use crate::tui::component::{Action, ClideComponent}; 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: &'a std::path::Path) -> Self { Self { explorer: Explorer::new(root_path), editor: Editor::new(), } } 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 { terminal.draw(|f| { f.render_widget(&mut self, f.area()); })?; // TODO: Handle events based on which component is active. if let Some(event) = self.get_event() { self.editor .event_handler .on_event(event.clone(), &mut self.editor.state); match event { Event::FocusGained => {} Event::FocusLost => {} Event::Key(key_event) => { // Handle main application key events. match self.handle_key_events(key_event) { Action::Noop => {} Action::Quit => break, Action::Pass => {} } } Event::Mouse(_) => {} Event::Paste(_) => {} Event::Resize(_, _) => {} } } } 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> ClideComponent for App<'a> { 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) } } } }