TUI #1
@ -3,6 +3,7 @@ mod component;
|
|||||||
mod editor;
|
mod editor;
|
||||||
mod explorer;
|
mod explorer;
|
||||||
mod logger;
|
mod logger;
|
||||||
|
mod title_bar;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::{LevelFilter, debug, info};
|
use log::{LevelFilter, debug, info};
|
||||||
|
|||||||
@ -3,11 +3,15 @@ use crate::tui::component::{Action, Component, Focus, FocusState};
|
|||||||
use crate::tui::editor::Editor;
|
use crate::tui::editor::Editor;
|
||||||
use crate::tui::explorer::Explorer;
|
use crate::tui::explorer::Explorer;
|
||||||
use crate::tui::logger::Logger;
|
use crate::tui::logger::Logger;
|
||||||
|
use crate::tui::title_bar::TitleBar;
|
||||||
|
use AppComponent::AppTitleBar;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::crossterm::event;
|
use ratatui::crossterm::event;
|
||||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use ratatui::crossterm::event::{
|
||||||
|
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
|
||||||
|
};
|
||||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::prelude::{Color, Style, Widget};
|
use ratatui::prelude::{Color, Style, Widget};
|
||||||
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap};
|
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap};
|
||||||
@ -23,12 +27,14 @@ pub enum AppComponent {
|
|||||||
AppEditor,
|
AppEditor,
|
||||||
AppExplorer,
|
AppExplorer,
|
||||||
AppLogger,
|
AppLogger,
|
||||||
|
AppTitleBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App<'a> {
|
pub struct App<'a> {
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
explorer: Explorer<'a>,
|
explorer: Explorer<'a>,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
title_bar: TitleBar,
|
||||||
last_active: AppComponent,
|
last_active: AppComponent,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +48,7 @@ impl<'a> App<'a> {
|
|||||||
editor: Editor::new(),
|
editor: Editor::new(),
|
||||||
explorer: Explorer::new(&root_path)?,
|
explorer: Explorer::new(&root_path)?,
|
||||||
logger: Logger::new(),
|
logger: Logger::new(),
|
||||||
|
title_bar: TitleBar::new(),
|
||||||
last_active: AppEditor,
|
last_active: AppEditor,
|
||||||
};
|
};
|
||||||
Ok(app)
|
Ok(app)
|
||||||
@ -82,20 +89,13 @@ impl<'a> App<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_top_status(&self, area: Rect, buf: &mut Buffer) {
|
|
||||||
// TODO: Status bar should have drop down menus
|
|
||||||
Tabs::new(["File", "Edit", "View", "Help"])
|
|
||||||
.style(Style::default())
|
|
||||||
.block(Block::default().borders(Borders::ALL))
|
|
||||||
.render(area, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) {
|
fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) {
|
||||||
// Determine help text from the most recently focused component.
|
// Determine help text from the most recently focused component.
|
||||||
let help = match self.last_active {
|
let help = match self.last_active {
|
||||||
AppEditor => self.editor.component_state.help_text.clone(),
|
AppEditor => self.editor.component_state.help_text.clone(),
|
||||||
AppExplorer => self.explorer.component_state.help_text.clone(),
|
AppExplorer => self.explorer.component_state.help_text.clone(),
|
||||||
AppLogger => self.logger.component_state.help_text.clone(),
|
AppLogger => self.logger.component_state.help_text.clone(),
|
||||||
|
AppTitleBar => self.title_bar.component_state.help_text.clone(),
|
||||||
};
|
};
|
||||||
Paragraph::new(
|
Paragraph::new(
|
||||||
concat!(
|
concat!(
|
||||||
@ -143,6 +143,7 @@ impl<'a> App<'a> {
|
|||||||
AppEditor => self.editor.component_state.set_focus(Focus::Active),
|
AppEditor => self.editor.component_state.set_focus(Focus::Active),
|
||||||
AppExplorer => self.explorer.component_state.set_focus(Focus::Active),
|
AppExplorer => self.explorer.component_state.set_focus(Focus::Active),
|
||||||
AppLogger => self.logger.component_state.set_focus(Focus::Active),
|
AppLogger => self.logger.component_state.set_focus(Focus::Active),
|
||||||
|
AppTitleBar => self.title_bar.component_state.set_focus(Focus::Active),
|
||||||
}
|
}
|
||||||
self.last_active = focus;
|
self.last_active = focus;
|
||||||
}
|
}
|
||||||
@ -198,7 +199,7 @@ impl<'a> Widget for &mut App<'a> {
|
|||||||
])
|
])
|
||||||
.split(horizontal[1]);
|
.split(horizontal[1]);
|
||||||
|
|
||||||
self.draw_top_status(vertical[0], buf);
|
self.title_bar.render(vertical[0], buf);
|
||||||
self.draw_bottom_status(vertical[3], buf);
|
self.draw_bottom_status(vertical[3], buf);
|
||||||
self.draw_tabs(editor_layout[0], buf);
|
self.draw_tabs(editor_layout[0], buf);
|
||||||
let id = App::id().to_string();
|
let id = App::id().to_string();
|
||||||
@ -226,9 +227,11 @@ impl<'a> Component for App<'a> {
|
|||||||
}
|
}
|
||||||
// Components should always handle mouse events for click interaction.
|
// Components should always handle mouse events for click interaction.
|
||||||
if let Some(mouse) = event.as_mouse_event() {
|
if let Some(mouse) = event.as_mouse_event() {
|
||||||
self.editor.handle_mouse_events(mouse)?;
|
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
|
||||||
self.explorer.handle_mouse_events(mouse)?;
|
self.editor.handle_mouse_events(mouse)?;
|
||||||
self.logger.handle_mouse_events(mouse)?;
|
self.explorer.handle_mouse_events(mouse)?;
|
||||||
|
self.logger.handle_mouse_events(mouse)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle events for all components.
|
// Handle events for all components.
|
||||||
@ -236,6 +239,7 @@ impl<'a> Component for App<'a> {
|
|||||||
AppEditor => self.editor.handle_event(event)?,
|
AppEditor => self.editor.handle_event(event)?,
|
||||||
AppExplorer => self.explorer.handle_event(event)?,
|
AppExplorer => self.explorer.handle_event(event)?,
|
||||||
AppLogger => self.logger.handle_event(event)?,
|
AppLogger => self.logger.handle_event(event)?,
|
||||||
|
AppTitleBar => self.title_bar.handle_event(event)?,
|
||||||
};
|
};
|
||||||
match action {
|
match action {
|
||||||
Action::Quit | Action::Handled => return Ok(action),
|
Action::Quit | Action::Handled => return Ok(action),
|
||||||
@ -274,6 +278,15 @@ impl<'a> Component for App<'a> {
|
|||||||
self.change_focus(AppLogger);
|
self.change_focus(AppLogger);
|
||||||
Ok(Action::Handled)
|
Ok(Action::Handled)
|
||||||
}
|
}
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('r'),
|
||||||
|
modifiers: KeyModifiers::ALT,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: _state,
|
||||||
|
} => {
|
||||||
|
self.change_focus(AppTitleBar);
|
||||||
|
Ok(Action::Handled)
|
||||||
|
}
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char('l'),
|
code: KeyCode::Char('l'),
|
||||||
modifiers: KeyModifiers::ALT,
|
modifiers: KeyModifiers::ALT,
|
||||||
|
|||||||
111
src/tui/title_bar.rs
Normal file
111
src/tui/title_bar.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use crate::tui::component::{Action, Component, ComponentState};
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::text::Line;
|
||||||
|
use ratatui::widgets::{Block, Borders, Tabs, Widget};
|
||||||
|
use strum::{EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)]
|
||||||
|
enum TitleBarItem {
|
||||||
|
File,
|
||||||
|
Edit,
|
||||||
|
View,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TitleBarItem {
|
||||||
|
pub fn next(mut self) -> Self {
|
||||||
|
let cur = self as usize;
|
||||||
|
let next = cur.saturating_add(1);
|
||||||
|
Self::from_repr(next).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(self) -> Self {
|
||||||
|
let cur = self as usize;
|
||||||
|
let prev = cur.saturating_sub(1);
|
||||||
|
Self::from_repr(prev).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
TitleBarItem::File => "File",
|
||||||
|
TitleBarItem::Edit => "Edit",
|
||||||
|
TitleBarItem::View => "View",
|
||||||
|
TitleBarItem::Help => "Help",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TitleBar {
|
||||||
|
selected: TitleBarItem,
|
||||||
|
opened: Option<TitleBarItem>,
|
||||||
|
pub(crate) component_state: ComponentState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TitleBar {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
selected: TitleBarItem::File,
|
||||||
|
opened: None,
|
||||||
|
component_state: ComponentState::default()
|
||||||
|
.with_help_text(concat!("TODO: Title bar help text.").as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let titles: Vec<Line> = TitleBarItem::iter()
|
||||||
|
.map(|item| Line::from(item.id().to_owned()))
|
||||||
|
.collect();
|
||||||
|
let tabs_style = Style::default();
|
||||||
|
let highlight_style = if self.opened.is_some() {
|
||||||
|
Style::default().bg(Color::Blue).fg(Color::White)
|
||||||
|
} else {
|
||||||
|
Style::default().bg(Color::Cyan).fg(Color::Black)
|
||||||
|
};
|
||||||
|
Tabs::new(titles)
|
||||||
|
.style(tabs_style)
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.highlight_style(highlight_style)
|
||||||
|
.select(self.selected as usize)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &TitleBar {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let title_bar_area = Rect {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: 3,
|
||||||
|
};
|
||||||
|
self.render_title_bar(title_bar_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for TitleBar {
|
||||||
|
fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result<Action> {
|
||||||
|
match key.code {
|
||||||
|
// KeyCode::Up | KeyCode::Char('k') => self.selected.key_up(),
|
||||||
|
// KeyCode::Down | KeyCode::Char('j') => self.selected.key_down(),
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => {
|
||||||
|
self.selected = self.selected.prev();
|
||||||
|
Ok(Action::Handled)
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => {
|
||||||
|
self.selected = self.selected.next();
|
||||||
|
Ok(Action::Handled)
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.opened = Some(self.selected);
|
||||||
|
Ok(Action::Handled)
|
||||||
|
}
|
||||||
|
_ => Ok(Action::Noop),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user