From 0c87fda7957285b0d105767bb9e359f43bbc14d6 Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Thu, 22 Jan 2026 19:23:21 -0500 Subject: [PATCH] [tui] Add basic support for focusing widgets. It's pretty bad but it allows to control which widget accepts input. --- Cargo.lock | 29 +++++++++--------- Cargo.toml | 1 + src/tui/app.rs | 70 ++++++++++++++++++++++++++++++++++++-------- src/tui/component.rs | 48 ++++++++++++++++++++++++++++++ src/tui/editor.rs | 8 ++++- src/tui/explorer.rs | 9 +++++- src/tui/logger.rs | 8 ++++- 7 files changed, 143 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 795325c..063f626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,7 @@ dependencies = [ "edtui", "log", "ratatui", + "strum", "syntect", "tui-logger", "tui-tree-widget", @@ -451,9 +452,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.192" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbda285ba6e5866529faf76352bdf73801d9b44a6308d7cd58ca2379f378e994" +checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" dependencies = [ "cc", "cxx-build", @@ -466,9 +467,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.192" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9efde466c5d532d57efd92f861da3bdb7f61e369128ce8b4c3fe0c9de4fa4d" +checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" dependencies = [ "cc", "codespan-reporting 0.13.1", @@ -481,9 +482,9 @@ dependencies = [ [[package]] name = "cxx-gen" -version = "0.7.192" +version = "0.7.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee08d1131e8f050a1d1acbb7c699e5c8d29c325dffc382331c280d99f98c2618" +checksum = "035b6c61a944483e8a4b2ad4fb8b13830d63491bd004943716ad16d85dcc64bc" dependencies = [ "codespan-reporting 0.13.1", "indexmap", @@ -564,9 +565,9 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.192" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3efb93799095bccd4f763ca07997dc39a69e5e61ab52d2c407d4988d21ce144d" +checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" dependencies = [ "clap", "codespan-reporting 0.13.1", @@ -578,15 +579,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.192" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3092010228026e143b32a4463ed9fa8f86dca266af4bf5f3b2a26e113dbe4e45" +checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" [[package]] name = "cxxbridge-macro" -version = "1.0.192" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d72ebfcd351ae404fb00ff378dfc9571827a00722c9e735c9181aec320ba0a" +checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" dependencies = [ "indexmap", "proc-macro2", @@ -1599,9 +1600,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index de9de16..c9b44f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ anyhow = "1.0.100" tui-tree-widget = "0.24.0" tui-logger = "0.18.1" edtui = "0.11.1" +strum = "0.27.2" [build-dependencies] # The link_qt_object_files feature is required for statically linking Qt 6. diff --git a/src/tui/app.rs b/src/tui/app.rs index 55b319e..bf244ab 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -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), } @@ -45,12 +46,13 @@ impl<'a> App<'a> { AppComponents::AppLogger(Logger::new()), ], }; - app.get_component_mut::() - .unwrap() + let editor = app.get_component_mut::().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 { 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::() + .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::() + .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::() + .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'), diff --git a/src/tui/component.rs b/src/tui/component.rs index 99dd7c9..0ac02dd 100644 --- a/src/tui/component.rs +++ b/src/tui/component.rs @@ -44,4 +44,52 @@ pub trait Component { fn update(&mut self, action: Action) -> Result { 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), + } + } } diff --git a/src/tui/editor.rs b/src/tui/editor.rs index d8c695e..5bba941 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -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, syntax_set: SyntaxSet, + pub(crate) component_state: ComponentState, } impl<'a> ComponentOf 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 { if let Some(key_event) = event.as_key_event() { // Handle events here that should not be passed on to the vim emulation handler. diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index 7e83632..fcb4a05 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -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, + pub(crate) component_state: ComponentState, } impl<'a> ComponentOf> 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 { if let Some(key_event) = event.as_key_event() { // Handle events here that should not be passed on to the vim emulation handler. diff --git a/src/tui/logger.rs b/src/tui/logger.rs index 70a7d23..abf33bb 100644 --- a/src/tui/logger.rs +++ b/src/tui/logger.rs @@ -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 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 { if let Some(key_event) = event.as_key_event() { return self.handle_key_events(key_event);