TUI #1
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<String> {
|
||||
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<Action> {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user