use crate::tui::component::{Action, Component}; use crate::tui::editor::Editor; use anyhow::{Context, Result}; use log::{trace, warn}; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::Rect; use ratatui::prelude::{Color, Style}; use ratatui::widgets::{Block, Borders, Padding, Tabs, Widget}; use std::collections::HashMap; // Render the tabs with keys as titles // Tab keys can be file names. // Render the editor using the key as a reference for lookup pub struct EditorTab { pub(crate) editors: HashMap, tab_order: Vec, current_editor: usize, } impl EditorTab { fn id() -> &'static str { "EditorTab" } pub fn new(path: &std::path::PathBuf) -> Self { trace!(target:Self::id(), "Building EditorTab with path {path:?}"); let tab_order = vec![path.to_string_lossy().to_string()]; Self { editors: HashMap::from([(tab_order.first().unwrap().to_owned(), Editor::new(path))]), tab_order, current_editor: 0, } } pub fn next_editor(&mut self) { let next = (self.current_editor + 1) % self.tab_order.len(); trace!(target:Self::id(), "Moving from {} to next editor tab at {}", self.current_editor, next); self.current_editor = next; } pub fn prev_editor(&mut self) { let prev = self .current_editor .checked_sub(1) .unwrap_or(self.tab_order.len() - 1); trace!(target:Self::id(), "Moving from {} to previous editor tab at {}", self.current_editor, prev); self.current_editor = prev; } pub fn current_editor(&self) -> Option<&Editor> { self.editors.get(&self.tab_order[self.current_editor]) } pub fn current_editor_mut(&mut self) -> Option<&mut Editor> { self.editors.get_mut(&self.tab_order[self.current_editor]) } pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> { trace!(target:Self::id(), "Opening new EditorTab with path {:?}", path); if self .editors .contains_key(&path.to_string_lossy().to_string()) { warn!(target:Self::id(), "EditorTab already opened with this file"); return Ok(()); } let path_str = path.to_string_lossy().to_string(); self.tab_order.push(path_str.clone()); let mut editor = Editor::new(path); editor.set_contents(path).context("Failed to open tab")?; self.editors.insert(path_str, editor); self.current_editor = self.tab_order.len() - 1; Ok(()) } pub fn render(&mut self, tabs_area: Rect, editor_area: Rect, buf: &mut Buffer) { // TODO: Only file name is displayed in tab title, so files with the same name in different // directories will appear confusing. let tab_titles = self.tab_order.iter().map(|t| { std::path::PathBuf::from(t) .file_name() .map(|f| f.to_string_lossy().to_string()) .unwrap_or("Unknown".to_string()) }); // Don't set border color based on ComponentState::focus, the Editor renders the border. Tabs::new(tab_titles) .select(self.current_editor) .divider("|") .block( Block::default() .borders(Borders::NONE) .padding(Padding::new(0, 0, 0, 0)), ) .highlight_style(Style::default().fg(Color::LightRed)) .render(tabs_area, buf); Widget::render(self, editor_area, buf); } } impl Widget for &mut EditorTab { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { if let Some(editor) = self.current_editor_mut() { editor.render(area, buf); } } } impl Component for EditorTab { fn handle_event(&mut self, event: Event) -> Result { if let Some(key) = event.as_key_event() { let action = self.handle_key_events(key)?; match action { Action::Quit | Action::Handled => return Ok(action), _ => {} } } self.current_editor_mut() .context("Failed to get current editor")? .handle_event(event) } fn handle_key_events(&mut self, key: KeyEvent) -> Result { match key { KeyEvent { code: KeyCode::Char('h'), modifiers: KeyModifiers::ALT, .. } | KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::ALT, .. } => { self.prev_editor(); Ok(Action::Handled) } KeyEvent { code: KeyCode::Char('l'), modifiers: KeyModifiers::ALT, .. } | KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::ALT, .. } => { self.next_editor(); Ok(Action::Handled) } _ => Ok(Action::Noop), } } }