diff --git a/src/tui/app.rs b/src/tui/app.rs index 9d0d220..970f9db 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -99,7 +99,9 @@ impl<'a> App<'a> { AppEditor => match self.editor_tabs.current_editor() { Some(editor) => editor.component_state.help_text.clone(), None => { - error!(target:Self::id(), "Failed to get Editor while drawing bottom status bar"); + if !self.editor_tabs.is_empty() { + error!(target:Self::id(), "Failed to get Editor while drawing bottom status bar"); + } "Failed to get current Editor while getting widget help text".to_string() } }, @@ -242,14 +244,12 @@ impl<'a> Component for App<'a> { AppMenuBar => self.menu_bar.handle_event(event.clone())?, }; - let editor = self - .editor_tabs - .current_editor_mut() - .context("Failed to get current editor while handling App events")?; // Components should always handle mouse events for click interaction. if let Some(mouse) = event.as_mouse_event() { if mouse.kind == MouseEventKind::Down(MouseButton::Left) { - editor.handle_mouse_events(mouse)?; + if let Some(editor) = self.editor_tabs.current_editor_mut() { + editor.handle_mouse_events(mouse)?; + } self.explorer.handle_mouse_events(mouse)?; self.logger.handle_mouse_events(mouse)?; } @@ -257,13 +257,20 @@ impl<'a> Component for App<'a> { match action { Action::Quit | Action::Handled => Ok(action), - Action::Save => match editor.save() { - Ok(_) => Ok(Action::Handled), - Err(e) => { - error!(target:Self::id(), "Failed to save editor contents: {e}"); + Action::Save => match self.editor_tabs.current_editor_mut() { + None => { + error!(target:Self::id(), "Failed to get current editor while handling App Action::Save"); Ok(Action::Noop) } + Some(editor) => match editor.save() { + Ok(_) => Ok(Action::Handled), + Err(e) => { + error!(target:Self::id(), "Failed to save editor contents: {e}"); + Ok(Action::Noop) + } + }, }, + Action::OpenTab => { if let Ok(path) = self.explorer.selected() { let path_buf = PathBuf::from(path); @@ -273,6 +280,22 @@ impl<'a> Component for App<'a> { Ok(Action::Noop) } } + Action::CloseTab => match self.editor_tabs.close_current_tab() { + Ok(_) => Ok(Action::Handled), + Err(_) => Ok(Action::Noop), + }, + Action::ReloadFile => { + trace!(target:Self::id(), "Reloading file for current editor"); + if let Some(editor) = self.editor_tabs.current_editor_mut() { + editor + .reload_contents() + .map(|_| Action::Handled) + .context("Failed to handle Action::ReloadFile") + } else { + error!(target:Self::id(), "Failed to get current editor while handling App Action::ReloadFile"); + Ok(Action::Noop) + } + } _ => Ok(Action::Noop), } } diff --git a/src/tui/component.rs b/src/tui/component.rs index eb8bd14..4487ea1 100644 --- a/src/tui/component.rs +++ b/src/tui/component.rs @@ -22,6 +22,11 @@ pub enum Action { /// The input was handled by a Component and should not be passed to the next component. Handled, OpenTab, + ReloadFile, + ShowHideExplorer, + ShowHideLogger, + About, + CloseTab, } pub trait Component { diff --git a/src/tui/editor.rs b/src/tui/editor.rs index 76f772c..5070d90 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -38,6 +38,17 @@ impl Editor { } } + pub fn reload_contents(&mut self) -> Result<()> { + trace!(target:Self::id(), "Reloading editor file contents {:?}", self.file_path); + match self.file_path.clone() { + None => { + error!(target:Self::id(), "Failed to reload editor contents with None file_path"); + bail!("Failed to reload editor contents with None file_path") + } + Some(path) => self.set_contents(&path), + } + } + pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { trace!(target:Self::id(), "Setting Editor contents from path {:?}", path); if let Ok(contents) = std::fs::read_to_string(path) { diff --git a/src/tui/editor_tab.rs b/src/tui/editor_tab.rs index 24d7ae5..9965b16 100644 --- a/src/tui/editor_tab.rs +++ b/src/tui/editor_tab.rs @@ -1,7 +1,7 @@ -use crate::tui::component::{Action, Component}; +use crate::tui::component::{Action, Component, Focus, FocusState}; use crate::tui::editor::Editor; -use anyhow::{Context, Result}; -use log::{trace, warn}; +use anyhow::{Context, Result, anyhow}; +use log::{error, info, trace, warn}; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::Rect; @@ -36,6 +36,7 @@ impl EditorTab { 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.set_tab_focus(Focus::Active, next); self.current_editor = next; } @@ -45,15 +46,64 @@ impl EditorTab { .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.set_tab_focus(Focus::Active, prev); self.current_editor = prev; } + pub fn get_editor_key(&self, index: usize) -> Option { + match self.tab_order.get(index) { + None => { + if !self.tab_order.is_empty() { + error!(target:Self::id(), "Failed to get editor tab key with invalid index {index}"); + } + None + } + Some(key) => Some(key.to_owned()), + } + } + pub fn current_editor(&self) -> Option<&Editor> { - self.editors.get(&self.tab_order[self.current_editor]) + self.editors.get(&self.get_editor_key(self.current_editor)?) } pub fn current_editor_mut(&mut self) -> Option<&mut Editor> { - self.editors.get_mut(&self.tab_order[self.current_editor]) + self.editors + .get_mut(&self.get_editor_key(self.current_editor)?) + } + + pub fn set_current_tab_focus(&mut self, focus: Focus) { + trace!(target:Self::id(), "Setting current tab {} focus to {:?}", self.current_editor, focus); + self.set_tab_focus(focus, self.current_editor) + } + + pub fn set_tab_focus(&mut self, focus: Focus, index: usize) { + trace!(target:Self::id(), "Setting tab {} focus to {:?}", index, focus); + if focus == Focus::Active && index != self.current_editor { + // If we are setting another tab to active, disable the current one. + trace!( + target:Self::id(), + "New tab {} focus set to Active; Setting current tab {} to Inactive", + index, + self.current_editor + ); + self.set_current_tab_focus(Focus::Inactive); + } + match self.get_editor_key(index) { + None => { + error!(target:Self::id(), "Failed setting tab focus for invalid key {index}"); + } + Some(key) => match self.editors.get_mut(&key) { + None => { + error!( + target:Self::id(), + "Failed to update tab focus at index {} with invalid key: {}", + self.current_editor, + self.tab_order[self.current_editor] + ) + } + Some(editor) => editor.component_state.set_focus(focus), + }, + } } pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> { @@ -75,6 +125,35 @@ impl EditorTab { Ok(()) } + pub fn close_current_tab(&mut self) -> Result<()> { + self.close_tab(self.current_editor) + } + + pub fn close_tab(&mut self, index: usize) -> Result<()> { + let key = self + .tab_order + .get(index) + .ok_or(anyhow!( + "Failed to get tab order with invalid index {index}" + ))? + .to_owned(); + match self.editors.remove(&key) { + None => { + error!(target:Self::id(), "Failed to remove editor tab {key} with invalid index {index}") + } + Some(_) => { + self.prev_editor(); + self.tab_order.remove(index); + info!(target:Self::id(), "Closed editor tab {key} at index {index}") + } + } + Ok(()) + } + + pub fn is_empty(&self) -> bool { + self.editors.is_empty() + } + 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. @@ -119,9 +198,10 @@ impl Component for EditorTab { _ => {} } } - self.current_editor_mut() - .context("Failed to get current editor")? - .handle_event(event) + if let Some(editor) = self.current_editor_mut() { + return editor.handle_event(event); + } + Ok(Action::Noop) } fn handle_key_events(&mut self, key: KeyEvent) -> Result { diff --git a/src/tui/menu_bar.rs b/src/tui/menu_bar.rs index d99fe5c..ac4d3b2 100644 --- a/src/tui/menu_bar.rs +++ b/src/tui/menu_bar.rs @@ -1,6 +1,6 @@ use crate::tui::component::{Action, Component, ComponentState, FocusState}; use crate::tui::menu_bar::MenuBarItemOption::{ - About, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, + About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, }; use log::trace; use ratatui::buffer::Buffer; @@ -23,6 +23,7 @@ enum MenuBarItem { #[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] enum MenuBarItemOption { Save, + CloseTab, Reload, Exit, ShowHideExplorer, @@ -39,6 +40,7 @@ impl MenuBarItemOption { ShowHideExplorer => "Show / hide explorer", ShowHideLogger => "Show / hide logger", About => "About", + CloseTab => "Close tab", } } } @@ -66,7 +68,7 @@ impl MenuBarItem { pub fn options(&self) -> &[MenuBarItemOption] { match self { - MenuBarItem::File => &[Save, Reload, Exit], + MenuBarItem::File => &[Save, CloseTab, Reload, Exit], MenuBarItem::View => &[ShowHideExplorer, ShowHideLogger], MenuBarItem::Help => &[About], } @@ -145,14 +147,14 @@ impl MenuBar { } fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect { - // TODO: X offset for item option? It's fine as-is, but it might look nicer. let rect = Rect { x: anchor.x, y: anchor.y + anchor.height, width: width.min(area.width), height, }; - trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect); + // TODO: X offset for item option? It's fine as-is, but it might look nicer. + // trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect); rect } } @@ -190,14 +192,15 @@ impl Component for MenuBar { } KeyCode::Enter => { if let Some(selected) = self.list_state.selected() { - let seletion = self.selected.options()[selected]; - return match seletion { + let selection = self.selected.options()[selected]; + return match selection { Save => Ok(Action::Save), Exit => Ok(Action::Quit), - Reload => Ok(Action::Noop), // TODO - ShowHideExplorer => Ok(Action::Noop), // TODO - ShowHideLogger => Ok(Action::Noop), // TODO - About => Ok(Action::Noop), // TODO + Reload => Ok(Action::ReloadFile), + ShowHideExplorer => Ok(Action::ShowHideExplorer), + ShowHideLogger => Ok(Action::ShowHideLogger), + About => Ok(Action::About), + CloseTab => Ok(Action::CloseTab), }; } Ok(Action::Noop)