TUI #1

Merged
shaunrd0 merged 73 commits from ui into master 2026-01-25 20:57:37 +00:00
2 changed files with 127 additions and 33 deletions
Showing only changes of commit 76fe09f39b - Show all commits

View File

@ -1,5 +1,5 @@
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger}; use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState}; use crate::tui::component::{Action, Component, Focus, FocusState, Visible, VisibleState};
use crate::tui::editor_tab::EditorTab; use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
@ -186,24 +186,49 @@ impl<'a> Widget for &mut App<'a> {
where where
Self: Sized, Self: Sized,
{ {
let vertical = Layout::default() let vertical_constraints = match self.logger.component_state.vis {
.direction(Direction::Vertical) Visible::Visible => {
.constraints([ vec![
Constraint::Length(3), // top status bar Constraint::Length(3), // top status bar
Constraint::Percentage(70), // horizontal layout Constraint::Percentage(70), // horizontal layout
Constraint::Percentage(30), // terminal Constraint::Fill(1), // terminal
Constraint::Length(3), // bottom status bar Constraint::Length(3), // bottom status bar
]) ]
}
Visible::Hidden => {
vec![
Constraint::Length(3), // top status bar
Constraint::Fill(1), // horizontal layout
Constraint::Length(3), // bottom status bar
]
}
};
let vertical = Layout::default()
.direction(Direction::Vertical)
.constraints(vertical_constraints)
.split(area); .split(area);
let horizontal = Layout::default() let horizontal_constraints = match self.explorer.component_state.vis {
.direction(Direction::Horizontal) Visible::Visible => {
.constraints([ vec![
Constraint::Max(30), // File explorer with a max width of 30 characters. Constraint::Max(30), // File explorer with a max width of 30 characters.
Constraint::Fill(1), // Editor fills the remaining space. Constraint::Fill(1), // Editor fills the remaining space.
]) ]
.split(vertical[1]); }
Visible::Hidden => {
vec![
Constraint::Fill(1), // Editor fills the remaining space.
]
}
};
// The index used for vertical here does not care if the Logger is Visible or not.
let horizontal = Layout::default()
.direction(Direction::Horizontal)
.constraints(horizontal_constraints)
.split(vertical[1]);
match self.explorer.component_state.vis {
Visible::Visible => {
let editor_layout = Layout::default() let editor_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
@ -211,16 +236,38 @@ impl<'a> Widget for &mut App<'a> {
Constraint::Fill(1), // Editor contents. Constraint::Fill(1), // Editor contents.
]) ])
.split(horizontal[1]); .split(horizontal[1]);
self.draw_bottom_status(vertical[3], buf);
self.editor_tabs self.editor_tabs
.render(editor_layout[0], editor_layout[1], buf); .render(editor_layout[0], editor_layout[1], buf);
self.explorer.render(horizontal[0], buf); self.explorer.render(horizontal[0], buf);
self.logger.render(vertical[2], buf); }
Visible::Hidden => {
let editor_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // Editor tabs.
Constraint::Fill(1), // Editor contents.
])
.split(horizontal[0]);
self.editor_tabs
.render(editor_layout[0], editor_layout[1], buf);
}
}
match self.logger.component_state.vis {
// Index 1 of vertical is rendered with the horizontal layout above.
Visible::Visible => {
self.logger.render(vertical[2], buf);
self.draw_bottom_status(vertical[3], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus. // The title bar is rendered last to overlay any popups created for drop-down menus.
self.menu_bar.render(vertical[0], buf); self.menu_bar.render(vertical[0], buf);
} }
Visible::Hidden => {
self.draw_bottom_status(vertical[2], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus.
self.menu_bar.render(vertical[0], buf);
}
}
}
} }
impl<'a> Component for App<'a> { impl<'a> Component for App<'a> {
@ -255,6 +302,7 @@ impl<'a> Component for App<'a> {
} }
} }
// Handle actions returned from widgets that may need context on other widgets or app state.
match action { match action {
Action::Quit | Action::Handled => Ok(action), Action::Quit | Action::Handled => Ok(action),
Action::Save => match self.editor_tabs.current_editor_mut() { Action::Save => match self.editor_tabs.current_editor_mut() {
@ -270,7 +318,6 @@ impl<'a> Component for App<'a> {
} }
}, },
}, },
Action::OpenTab => { Action::OpenTab => {
if let Ok(path) = self.explorer.selected() { if let Ok(path) = self.explorer.selected() {
let path_buf = PathBuf::from(path); let path_buf = PathBuf::from(path);
@ -296,6 +343,14 @@ impl<'a> Component for App<'a> {
Ok(Action::Noop) Ok(Action::Noop)
} }
} }
Action::ShowHideLogger => {
self.logger.component_state.togget_visible();
Ok(Action::Handled)
}
Action::ShowHideExplorer => {
self.explorer.component_state.togget_visible();
Ok(Action::Handled)
}
_ => Ok(Action::Noop), _ => Ok(Action::Noop),
} }
} }

View File

@ -1,5 +1,7 @@
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
use crate::tui::component::Focus::Inactive;
use Focus::Active;
use anyhow::Result; use anyhow::Result;
use log::trace; use log::trace;
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
@ -58,6 +60,7 @@ pub trait Component {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ComponentState { pub struct ComponentState {
pub(crate) focus: Focus, pub(crate) focus: Focus,
pub(crate) vis: Visible,
pub(crate) help_text: String, pub(crate) help_text: String,
} }
@ -69,7 +72,8 @@ impl ComponentState {
fn new() -> Self { fn new() -> Self {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::id(), "Building {}", Self::id());
Self { Self {
focus: Focus::Active, focus: Active,
vis: Visible::Visible,
help_text: String::new(), help_text: String::new(),
} }
} }
@ -90,8 +94,8 @@ pub enum Focus {
impl Focus { impl Focus {
pub(crate) fn get_active_color(&self) -> Color { pub(crate) fn get_active_color(&self) -> Color {
match self { match self {
Focus::Active => Color::LightYellow, Active => Color::LightYellow,
Focus::Inactive => Color::White, Inactive => Color::White,
} }
} }
} }
@ -107,6 +111,7 @@ impl FocusState for ComponentState {
fn with_focus(self, focus: Focus) -> Self { fn with_focus(self, focus: Focus) -> Self {
Self { Self {
focus, focus,
vis: Visible::Visible,
help_text: self.help_text, help_text: self.help_text,
} }
} }
@ -117,8 +122,8 @@ impl FocusState for ComponentState {
fn toggle_focus(&mut self) { fn toggle_focus(&mut self) {
match self.focus { match self.focus {
Focus::Active => self.set_focus(Focus::Inactive), Active => self.set_focus(Inactive),
Focus::Inactive => self.set_focus(Focus::Active), Inactive => self.set_focus(Active),
} }
} }
@ -126,3 +131,37 @@ impl FocusState for ComponentState {
self.focus.get_active_color() self.focus.get_active_color()
} }
} }
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum Visible {
#[default]
Visible,
Hidden,
}
pub trait VisibleState {
fn with_visible(self, vis: Visible) -> Self;
fn set_visible(&mut self, vis: Visible);
fn togget_visible(&mut self);
}
impl VisibleState for ComponentState {
fn with_visible(self, vis: Visible) -> Self {
Self {
focus: self.focus,
vis,
help_text: self.help_text,
}
}
fn set_visible(&mut self, vis: Visible) {
self.vis = vis;
}
fn togget_visible(&mut self) {
match self.vis {
Visible::Visible => self.set_visible(Visible::Hidden),
Visible::Hidden => self.set_visible(Visible::Visible),
}
}
}