[tui] Add basic support for focusing widgets.
It's pretty bad but it allows to control which widget accepts input.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use crate::tui::component::{Action, Component};
|
||||
use crate::tui::component::{Action, Component, Focus, FocusState};
|
||||
use crate::tui::editor::Editor;
|
||||
use crate::tui::explorer::Explorer;
|
||||
use crate::tui::logger::Logger;
|
||||
@@ -21,6 +21,7 @@ pub enum AppComponents<'a> {
|
||||
AppEditor(Editor),
|
||||
AppExplorer(Explorer<'a>),
|
||||
AppLogger(Logger),
|
||||
#[allow(dead_code)]
|
||||
AppComponent(Box<dyn Component>),
|
||||
}
|
||||
|
||||
@@ -45,12 +46,13 @@ impl<'a> App<'a> {
|
||||
AppComponents::AppLogger(Logger::new()),
|
||||
],
|
||||
};
|
||||
app.get_component_mut::<Editor>()
|
||||
.unwrap()
|
||||
let editor = app.get_component_mut::<Editor>().unwrap();
|
||||
editor
|
||||
.set_contents(&root_path.join("src/tui/app.rs"))
|
||||
.context(format!(
|
||||
"Failed to initialize editor contents to path: {root_path:?}"
|
||||
))?;
|
||||
editor.component_state.set_focus(Focus::Active);
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
@@ -219,13 +221,20 @@ impl<'a> Component for App<'a> {
|
||||
|
||||
// Handle events for all components.
|
||||
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())?,
|
||||
AppComponents::AppLogger(logger) => logger.handle_event(event.clone())?,
|
||||
let c = match component {
|
||||
AppComponents::AppEditor(e) => e as &mut dyn Component,
|
||||
AppComponents::AppExplorer(e) => e as &mut dyn Component,
|
||||
AppComponents::AppLogger(e) => e as &mut dyn Component,
|
||||
AppComponents::AppComponent(e) => e.as_mut() as &mut dyn Component,
|
||||
};
|
||||
// Actions returned here abort the input handling iteration.
|
||||
if !c.is_active() {
|
||||
if let Some(mouse) = event.as_mouse_event() {
|
||||
// Always handle mouse events for click interaction.
|
||||
c.handle_mouse_events(mouse)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let action = c.handle_event(event.clone())?;
|
||||
match action {
|
||||
Action::Quit | Action::Handled => return Ok(action),
|
||||
_ => {}
|
||||
@@ -238,18 +247,53 @@ impl<'a> Component for App<'a> {
|
||||
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
|
||||
match key {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('l'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
code: KeyCode::Char('q'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _state,
|
||||
} => {
|
||||
self.get_component_mut::<Explorer>()
|
||||
.unwrap()
|
||||
.component_state
|
||||
.toggle_focus();
|
||||
Ok(Action::Handled)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _state,
|
||||
} => {
|
||||
self.get_component_mut::<Editor>()
|
||||
.unwrap()
|
||||
.component_state
|
||||
.toggle_focus();
|
||||
Ok(Action::Handled)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _state,
|
||||
} => {
|
||||
self.get_component_mut::<Logger>()
|
||||
.unwrap()
|
||||
.component_state
|
||||
.toggle_focus();
|
||||
Ok(Action::Handled)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('l'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _state,
|
||||
} => {
|
||||
// Some example logs for testing.
|
||||
error!(target:self.id(), "an error");
|
||||
warn!(target:self.id(), "a warning");
|
||||
info!(target:self.id(), "a two line info\nsecond line");
|
||||
debug!(target:self.id(), "a debug");
|
||||
trace!(target:self.id(), "a trace");
|
||||
Ok(Action::Noop)
|
||||
Ok(Action::Handled)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
|
||||
@@ -44,4 +44,52 @@ pub trait Component {
|
||||
fn update(&mut self, action: Action) -> Result<Action> {
|
||||
Ok(Action::Noop)
|
||||
}
|
||||
|
||||
/// Override this method for creating components that conditionally handle input.
|
||||
fn is_active(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ComponentState {
|
||||
pub(crate) focus: Focus,
|
||||
}
|
||||
|
||||
impl ComponentState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
focus: Focus::Active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub enum Focus {
|
||||
Active,
|
||||
#[default]
|
||||
Inactive,
|
||||
}
|
||||
|
||||
pub trait FocusState {
|
||||
fn with_focus(self, focus: Focus) -> Self;
|
||||
fn set_focus(&mut self, focus: Focus);
|
||||
fn toggle_focus(&mut self);
|
||||
}
|
||||
|
||||
impl FocusState for ComponentState {
|
||||
fn with_focus(self, focus: Focus) -> Self {
|
||||
Self { focus }
|
||||
}
|
||||
|
||||
fn set_focus(&mut self, focus: Focus) {
|
||||
self.focus = focus;
|
||||
}
|
||||
|
||||
fn toggle_focus(&mut self) {
|
||||
match self.focus {
|
||||
Focus::Active => self.set_focus(Focus::Inactive),
|
||||
Focus::Inactive => self.set_focus(Focus::Active),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::tui::component::{Action, Component};
|
||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||
|
||||
use crate::tui::app::{AppComponents, ComponentOf};
|
||||
use anyhow::{Context, Result, bail};
|
||||
@@ -21,6 +21,7 @@ pub struct Editor {
|
||||
pub event_handler: EditorEventHandler,
|
||||
pub file_path: Option<std::path::PathBuf>,
|
||||
syntax_set: SyntaxSet,
|
||||
pub(crate) component_state: ComponentState,
|
||||
}
|
||||
|
||||
impl<'a> ComponentOf<Editor> for AppComponents<'a> {
|
||||
@@ -45,6 +46,7 @@ impl Editor {
|
||||
event_handler: EditorEventHandler::default(),
|
||||
file_path: None,
|
||||
syntax_set: SyntaxSet::load_defaults_nonewlines(),
|
||||
component_state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +108,10 @@ impl Component for Editor {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.component_state.focus == Focus::Active
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> Result<Action> {
|
||||
if let Some(key_event) = event.as_key_event() {
|
||||
// Handle events here that should not be passed on to the vim emulation handler.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::tui::app::{AppComponents, ComponentOf};
|
||||
use crate::tui::component::{Action, Component};
|
||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
|
||||
@@ -15,6 +15,7 @@ pub struct Explorer<'a> {
|
||||
root_path: std::path::PathBuf,
|
||||
tree_items: TreeItem<'a, String>,
|
||||
tree_state: TreeState<String>,
|
||||
pub(crate) component_state: ComponentState,
|
||||
}
|
||||
|
||||
impl<'a> ComponentOf<Explorer<'a>> for AppComponents<'a> {
|
||||
@@ -38,6 +39,7 @@ impl<'a> Explorer<'a> {
|
||||
root_path: path.to_owned(),
|
||||
tree_items: Self::build_tree_from_path(path.to_owned())?,
|
||||
tree_state: TreeState::default(),
|
||||
component_state: Default::default(),
|
||||
};
|
||||
Ok(explorer)
|
||||
}
|
||||
@@ -131,6 +133,11 @@ impl<'a> Component for Explorer<'a> {
|
||||
fn id(&self) -> &str {
|
||||
"Explorer"
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.component_state.focus == Focus::Active
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> Result<Action> {
|
||||
if let Some(key_event) = event.as_key_event() {
|
||||
// Handle events here that should not be passed on to the vim emulation handler.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::tui::app::{AppComponents, ComponentOf};
|
||||
use crate::tui::component::{Action, Component};
|
||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent};
|
||||
use ratatui::layout::Rect;
|
||||
@@ -11,6 +11,7 @@ use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetEvent, Tui
|
||||
/// The logger is bound to info!, debug!, error!, trace! macros within Tui::new().
|
||||
pub struct Logger {
|
||||
state: TuiWidgetState,
|
||||
pub(crate) component_state: ComponentState,
|
||||
}
|
||||
|
||||
impl<'a> ComponentOf<Logger> for AppComponents<'a> {
|
||||
@@ -32,6 +33,7 @@ impl Logger {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: TuiWidgetState::new(),
|
||||
component_state: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +66,10 @@ impl Component for Logger {
|
||||
"Logger"
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.component_state.focus == Focus::Active
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> anyhow::Result<Action> {
|
||||
if let Some(key_event) = event.as_key_event() {
|
||||
return self.handle_key_events(key_event);
|
||||
|
||||
Reference in New Issue
Block a user