diff --git a/src/tui.rs b/src/tui.rs index fc67c72..6deb805 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -3,6 +3,7 @@ mod component; mod editor; mod explorer; mod logger; +mod title_bar; use anyhow::{Context, Result}; use log::{LevelFilter, debug, info}; diff --git a/src/tui/app.rs b/src/tui/app.rs index db339ef..ff8f2b5 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -3,11 +3,15 @@ use crate::tui::component::{Action, Component, Focus, FocusState}; use crate::tui::editor::Editor; use crate::tui::explorer::Explorer; use crate::tui::logger::Logger; +use crate::tui::title_bar::TitleBar; +use AppComponent::AppTitleBar; use anyhow::{Context, Result}; use log::{debug, error, info, trace, warn}; use ratatui::buffer::Buffer; use ratatui::crossterm::event; -use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use ratatui::crossterm::event::{ + Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind, +}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::prelude::{Color, Style, Widget}; use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap}; @@ -23,12 +27,14 @@ pub enum AppComponent { AppEditor, AppExplorer, AppLogger, + AppTitleBar, } pub struct App<'a> { editor: Editor, explorer: Explorer<'a>, logger: Logger, + title_bar: TitleBar, last_active: AppComponent, } @@ -42,6 +48,7 @@ impl<'a> App<'a> { editor: Editor::new(), explorer: Explorer::new(&root_path)?, logger: Logger::new(), + title_bar: TitleBar::new(), last_active: AppEditor, }; Ok(app) @@ -82,20 +89,13 @@ impl<'a> App<'a> { Ok(()) } - fn draw_top_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_bottom_status(&self, area: Rect, buf: &mut Buffer) { // Determine help text from the most recently focused component. let help = match self.last_active { AppEditor => self.editor.component_state.help_text.clone(), AppExplorer => self.explorer.component_state.help_text.clone(), AppLogger => self.logger.component_state.help_text.clone(), + AppTitleBar => self.title_bar.component_state.help_text.clone(), }; Paragraph::new( concat!( @@ -143,6 +143,7 @@ impl<'a> App<'a> { AppEditor => self.editor.component_state.set_focus(Focus::Active), AppExplorer => self.explorer.component_state.set_focus(Focus::Active), AppLogger => self.logger.component_state.set_focus(Focus::Active), + AppTitleBar => self.title_bar.component_state.set_focus(Focus::Active), } self.last_active = focus; } @@ -198,7 +199,7 @@ impl<'a> Widget for &mut App<'a> { ]) .split(horizontal[1]); - self.draw_top_status(vertical[0], buf); + self.title_bar.render(vertical[0], buf); self.draw_bottom_status(vertical[3], buf); self.draw_tabs(editor_layout[0], buf); let id = App::id().to_string(); @@ -226,9 +227,11 @@ impl<'a> Component for App<'a> { } // Components should always handle mouse events for click interaction. if let Some(mouse) = event.as_mouse_event() { - self.editor.handle_mouse_events(mouse)?; - self.explorer.handle_mouse_events(mouse)?; - self.logger.handle_mouse_events(mouse)?; + if mouse.kind == MouseEventKind::Down(MouseButton::Left) { + self.editor.handle_mouse_events(mouse)?; + self.explorer.handle_mouse_events(mouse)?; + self.logger.handle_mouse_events(mouse)?; + } } // Handle events for all components. @@ -236,6 +239,7 @@ impl<'a> Component for App<'a> { AppEditor => self.editor.handle_event(event)?, AppExplorer => self.explorer.handle_event(event)?, AppLogger => self.logger.handle_event(event)?, + AppTitleBar => self.title_bar.handle_event(event)?, }; match action { Action::Quit | Action::Handled => return Ok(action), @@ -274,6 +278,15 @@ impl<'a> Component for App<'a> { self.change_focus(AppLogger); Ok(Action::Handled) } + KeyEvent { + code: KeyCode::Char('r'), + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: _state, + } => { + self.change_focus(AppTitleBar); + Ok(Action::Handled) + } KeyEvent { code: KeyCode::Char('l'), modifiers: KeyModifiers::ALT, diff --git a/src/tui/title_bar.rs b/src/tui/title_bar.rs new file mode 100644 index 0000000..b4659d0 --- /dev/null +++ b/src/tui/title_bar.rs @@ -0,0 +1,111 @@ +use crate::tui::component::{Action, Component, ComponentState}; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Tabs, Widget}; +use strum::{EnumIter, FromRepr, IntoEnumIterator}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] +enum TitleBarItem { + File, + Edit, + View, + Help, +} + +impl TitleBarItem { + pub fn next(mut self) -> Self { + let cur = self as usize; + let next = cur.saturating_add(1); + Self::from_repr(next).unwrap_or(self) + } + + pub fn prev(self) -> Self { + let cur = self as usize; + let prev = cur.saturating_sub(1); + Self::from_repr(prev).unwrap_or(self) + } + + pub fn id(&self) -> &str { + match self { + TitleBarItem::File => "File", + TitleBarItem::Edit => "Edit", + TitleBarItem::View => "View", + TitleBarItem::Help => "Help", + } + } +} + +pub struct TitleBar { + selected: TitleBarItem, + opened: Option, + pub(crate) component_state: ComponentState, +} + +impl TitleBar { + pub fn new() -> Self { + Self { + selected: TitleBarItem::File, + opened: None, + component_state: ComponentState::default() + .with_help_text(concat!("TODO: Title bar help text.").as_ref()), + } + } + + fn render_title_bar(&self, area: Rect, buf: &mut Buffer) { + let titles: Vec = TitleBarItem::iter() + .map(|item| Line::from(item.id().to_owned())) + .collect(); + let tabs_style = Style::default(); + let highlight_style = if self.opened.is_some() { + Style::default().bg(Color::Blue).fg(Color::White) + } else { + Style::default().bg(Color::Cyan).fg(Color::Black) + }; + Tabs::new(titles) + .style(tabs_style) + .block(Block::default().borders(Borders::ALL)) + .highlight_style(highlight_style) + .select(self.selected as usize) + .render(area, buf); + } +} + +impl Widget for &TitleBar { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let title_bar_area = Rect { + x: area.x, + y: area.y, + width: area.width, + height: 3, + }; + self.render_title_bar(title_bar_area, buf); + } +} + +impl Component for TitleBar { + fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result { + match key.code { + // KeyCode::Up | KeyCode::Char('k') => self.selected.key_up(), + // KeyCode::Down | KeyCode::Char('j') => self.selected.key_down(), + KeyCode::Left | KeyCode::Char('h') => { + self.selected = self.selected.prev(); + Ok(Action::Handled) + } + KeyCode::Right | KeyCode::Char('l') => { + self.selected = self.selected.next(); + Ok(Action::Handled) + } + KeyCode::Enter => { + self.opened = Some(self.selected); + Ok(Action::Handled) + } + _ => Ok(Action::Noop), + } + } +}