[tui] Add AppComponent enum for storing all components.

This commit is contained in:
Shaun Reed 2026-01-20 16:03:38 -05:00
parent 3ffdcc2865
commit ce2949159c
3 changed files with 108 additions and 52 deletions

View File

@ -1,6 +1,7 @@
use crate::tui::component::{Action, Component};
use crate::tui::editor::Editor;
use crate::tui::explorer::Explorer;
use anyhow::{Result, anyhow};
use ratatui::buffer::Buffer;
use ratatui::crossterm::event;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
@ -11,23 +12,71 @@ use ratatui::{DefaultTerminal, symbols};
use std::path::PathBuf;
use std::time::Duration;
pub enum AppComponents<'a> {
AppEditor(Editor),
AppExplorer(Explorer<'a>),
AppComponent(Box<dyn Component>),
}
pub struct App<'a> {
explorer: Explorer<'a>,
editor: Editor,
components: Vec<AppComponents<'a>>,
}
impl<'a> App<'a> {
pub(crate) fn new(root_path: PathBuf) -> Self {
let mut app = Self {
explorer: Explorer::new(&root_path),
editor: Editor::new(),
components: vec![
AppComponents::AppExplorer(Explorer::new(&root_path)),
AppComponents::AppEditor(Editor::new()),
],
};
app.editor
app.get_editor_mut()
.unwrap()
.set_contents(&root_path.join("src/tui/app.rs"))
.expect("Failed to set editor contents.");
app
}
fn get_explorer(&self) -> Result<&Explorer<'a>> {
for component in &self.components {
if let AppComponents::AppExplorer(explorer) = component {
return Ok(explorer);
}
}
Err(anyhow::anyhow!("Failed to find project explorer widget."))
}
fn get_explorer_mut(&mut self) -> Result<&mut Explorer<'a>> {
for component in &mut self.components {
if let AppComponents::AppExplorer(explorer) = component {
return Ok(explorer);
}
}
Err(anyhow::anyhow!("Failed to find project explorer widget."))
}
fn get_editor(&self) -> Option<&Editor> {
for component in &self.components {
if let AppComponents::AppEditor(editor) = component {
return Some(editor);
}
}
// There is no editor currently opened.
None
}
fn get_editor_mut(&mut self) -> Option<&mut Editor> {
for component in &mut self.components {
if let AppComponents::AppEditor(editor) = component {
return Some(editor);
}
}
// There is no editor currently opened.
None
}
fn get_event(&mut self) -> Option<Event> {
if !event::poll(Duration::from_millis(250)).expect("event poll failed") {
return None;
@ -36,7 +85,7 @@ impl<'a> App<'a> {
event::read().ok()
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> {
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
loop {
// TODO: Handle events based on which component is active.
terminal.draw(|f| {
@ -66,15 +115,16 @@ impl<'a> App<'a> {
fn draw_tabs(&self, area: Rect, buf: &mut Buffer) {
// Determine the tab title from the current file (or use a fallback).
let title = self
.editor
let mut title: Option<&str> = None;
if let Some(editor) = self.get_editor() {
title = editor
.file_path
.as_ref()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
.unwrap_or("Untitled");
}
Tabs::new(vec![title])
Tabs::new(vec![title.unwrap_or("Unknown")])
.divider(symbols::DOT)
.block(
Block::default()
@ -102,17 +152,22 @@ impl<'a> App<'a> {
/// Refresh the contents of the editor to match the selected TreeItem in the file Explorer.
/// If the selected item is not a file, this does nothing.
fn refresh_editor_contents(&mut self) {
if let Some(current_file_path) = self.editor.file_path.clone() {
if let Some(selected_path_string) = self.explorer.selected() {
let selected_pathbuf = PathBuf::from(selected_path_string);
if std::path::absolute(&selected_pathbuf).unwrap().is_file()
&& selected_pathbuf != current_file_path
{
self.editor.set_contents(&selected_pathbuf.into()).ok();
}
fn refresh_editor_contents(&mut self) -> Result<()> {
// Use the currently selected TreeItem or get an absolute path to this source file.
let selected_pathbuf = match self.get_explorer()?.selected() {
Ok(path) => PathBuf::from(path),
Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()),
};
let editor = self
.get_editor_mut()
.expect("Failed to get active editor while refreshing contents.");
if let Some(current_file_path) = editor.file_path.clone() {
if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() {
return Ok(());
}
return editor.set_contents(&selected_pathbuf);
}
Err(anyhow!("Failed to refresh editor contents"))
}
}
@ -149,12 +204,13 @@ impl<'a> Widget for &mut App<'a> {
self.draw_status(vertical[0], buf);
self.draw_terminal(vertical[2], buf);
self.explorer.render(horizontal[0], buf);
if let Ok(explorer) = self.get_explorer_mut() {
explorer.render(horizontal[0], buf);
}
self.draw_tabs(editor_layout[0], buf);
self.refresh_editor_contents();
self.editor.render(editor_layout[1], buf);
self.refresh_editor_contents()
.expect("Failed to refresh editor contents.");
self.get_editor_mut().unwrap().render(editor_layout[1], buf);
}
}
@ -169,34 +225,35 @@ impl<'a> Component for App<'a> {
///
/// App could then provide helpers for altering Component state based on TUI grouping..
/// (such as editor tabs, file explorer, status bars, etc..)
///
/// Handles events for the App and delegates to attached Components.
fn handle_event(&mut self, event: Event) -> Action {
// Handle events in the primary application.
if let Some(key_event) = event.as_key_event() {
match self.handle_key_events(key_event) {
Action::Quit => return Action::Quit,
Action::Handled => {
// dbg!(format!("Handled event: {:?}", self.id()));
return Action::Handled;
}
Action::Handled => return Action::Handled,
_ => {}
}
}
self.explorer.handle_event(event.clone());
self.editor.handle_event(event.clone());
// Handle events for all components.
// for component in &mut self.components {
// dbg!(format!("Handling event: {:?}", component.id()));
// // Actions returned here abort the input handling iteration.
// match component.handle_event(event.clone()) {
// Action::Quit => return Action::Quit,
// Action::Handled => return Action::Handled,
// _ => continue,
// }
// }
for component in &mut self.components {
let action = match component {
AppComponents::AppEditor(editor) => editor.handle_event(event.clone()),
AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone()),
AppComponents::AppComponent(comp) => comp.handle_event(event.clone()),
};
// Actions returned here abort the input handling iteration.
match action {
Action::Quit | Action::Handled => return action,
_ => {}
}
}
Action::Noop
}
/// Handles key events for the App Component only.
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
match key {
KeyEvent {
@ -205,10 +262,7 @@ impl<'a> Component for App<'a> {
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)
}
_ => Action::Noop,
}
}
}

View File

@ -1,6 +1,6 @@
use crate::tui::component::{Action, Component};
use anyhow::Result;
use anyhow::{Context, Result};
use edtui::{
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
};
@ -40,9 +40,8 @@ impl Editor {
.collect();
self.file_path = Some(path.clone());
self.state.lines = Lines::new(lines);
return Ok(());
}
Err(anyhow::Error::msg("Failed to set editor file contents"))
Ok(())
}
pub fn save(&self) -> Result<()> {

View File

@ -102,8 +102,11 @@ impl<'a> Explorer<'a> {
)
}
pub fn selected(&self) -> Option<&String> {
self.tree_state.selected().last()
pub fn selected(&self) -> Result<String> {
if let Some(path) = self.tree_state.selected().last() {
return Ok(std::path::absolute(path)?.to_str().unwrap().to_string());
}
Err(anyhow::anyhow!("Failed to get selected TreeItem"))
}
}