diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ca0eaac2..1344498e86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895)) * when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748)) * index-out-of-bounds panic when unstaging lines near the end of a diff ([#2953](https://github.com/gitui-org/gitui/issues/2953)) +* restore highlights on find tool ([#3004](https://github.com/gitui-org/gitui/issues/3004)) ## [0.28.1] - 2026-03-21 diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 357fef1df1..618f9a6916 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -131,9 +131,34 @@ impl TextInputComponent { self.embed = true; } - /// - pub const fn enabled(&mut self, enable: bool) { + /// Focus/unfocus the text input visually (style + cursor). + /// Used by popups that share the field with other selectable rows. + pub fn enabled(&mut self, enable: bool) { self.selected = Some(enable); + self.apply_enabled_style(); + } + + fn is_enabled(&self) -> bool { + self.selected.unwrap_or(true) + } + + fn apply_enabled_style(&mut self) { + let enabled = self.is_enabled(); + let Some(ta) = self.textarea.as_mut() else { + return; + }; + + let style = self.theme.text(enabled, false); + ta.set_style(style); + ta.set_placeholder_style(style); + // Hide the block cursor when another control has focus. + // Default REVERSED cursor is what ratatui-textarea uses when focused. + ta.set_cursor_style(if enabled { + ratatui::style::Style::default() + .add_modifier(ratatui::style::Modifier::REVERSED) + } else { + ratatui::style::Style::default() + }); } fn show_inner_textarea(&mut self) { @@ -156,13 +181,6 @@ impl TextInputComponent { text_area .set_cursor_line_style(self.theme.text(true, false)); text_area.set_placeholder_text(self.default_msg.clone()); - text_area.set_placeholder_style( - self.theme - .text(self.selected.unwrap_or_default(), false), - ); - text_area.set_style( - self.theme.text(self.selected.unwrap_or(true), false), - ); if !self.embed { text_area.set_block( @@ -179,6 +197,8 @@ impl TextInputComponent { } text_area }); + // Apply focus style/cursor after the widget exists (and on rebuilds). + self.apply_enabled_style(); } /// Set the `msg`. @@ -887,4 +907,30 @@ mod tests { assert_eq!(ta.cursor(), save_cursor); } } + + #[test] + fn test_enabled_toggles_cursor_visibility() { + use ratatui::style::Modifier; + + let env = Environment::test_env(); + let mut comp = TextInputComponent::new(&env, "", "", false); + comp.show_inner_textarea(); + comp.enabled(true); + assert!(comp + .textarea + .as_ref() + .unwrap() + .cursor_style() + .add_modifier + .contains(Modifier::REVERSED)); + + comp.enabled(false); + assert!(!comp + .textarea + .as_ref() + .unwrap() + .cursor_style() + .add_modifier + .contains(Modifier::REVERSED)); + } } diff --git a/src/popups/log_search.rs b/src/popups/log_search.rs index 6f4602cd8a..7bac232003 100644 --- a/src/popups/log_search.rs +++ b/src/popups/log_search.rs @@ -107,7 +107,8 @@ impl LogSearchPopupPopup { self.mode = PopupMode::JumpCommitSha; self.jump_commit_id = None; self.find_text.set_default_msg("commit sha".into()); - self.find_text.enabled(false); + // Field stays focused; invalid SHA is shown via block style. + self.find_text.enabled(true); self.selection = Selection::EnterText; } } @@ -164,106 +165,66 @@ impl LogSearchPopupPopup { } } - fn get_text_options(&self) -> Vec> { - let x_summary = - if self.options.0.contains(SearchFields::MESSAGE_SUMMARY) - { - "X" - } else { - " " - }; - - let x_body = - if self.options.0.contains(SearchFields::MESSAGE_BODY) { - "X" - } else { - " " - }; - - let x_files = - if self.options.0.contains(SearchFields::FILENAMES) { - "X" - } else { - " " - }; - - let x_authors = - if self.options.0.contains(SearchFields::AUTHORS) { - "X" - } else { - " " - }; - - let x_opt_fuzzy = - if self.options.1.contains(SearchOptions::FUZZY_SEARCH) { - "X" - } else { - " " - }; + const fn option_mark(on: bool) -> &'static str { + if on { + "X" + } else { + " " + } + } - let x_opt_casesensitive = - if self.options.1.contains(SearchOptions::CASE_SENSITIVE) - { - "X" - } else { - " " - }; + fn option_line( + &self, + checked: bool, + label: &str, + selected: bool, + ) -> Line<'_> { + Line::from(vec![Span::styled( + format!("[{}] {label}", Self::option_mark(checked)), + // enabled=true keeps normal fg; selected applies selection_bg + self.theme.text(true, selected), + )]) + } + fn get_text_options(&self) -> Vec> { vec![ - Line::from(vec![Span::styled( - format!("[{x_opt_fuzzy}] fuzzy search"), - self.theme.text( - matches!(self.selection, Selection::FuzzyOption), - false, - ), - )]), - Line::from(vec![Span::styled( - format!("[{x_opt_casesensitive}] case sensitive"), - self.theme.text( - matches!(self.selection, Selection::CaseOption), - false, - ), - )]), - Line::from(vec![Span::styled( - format!("[{x_summary}] summary"), - self.theme.text( - matches!( - self.selection, - Selection::SummarySearch - ), - false, - ), - )]), - Line::from(vec![Span::styled( - format!("[{x_body}] message body"), - self.theme.text( - matches!( - self.selection, - Selection::MessageBodySearch - ), - false, - ), - )]), - Line::from(vec![Span::styled( - format!("[{x_files}] committed files"), - self.theme.text( - matches!( - self.selection, - Selection::FilenameSearch - ), - false, - ), - )]), - Line::from(vec![Span::styled( - format!("[{x_authors}] authors"), - self.theme.text( - matches!( - self.selection, - Selection::AuthorsSearch - ), - false, + self.option_line( + self.options.1.contains(SearchOptions::FUZZY_SEARCH), + "fuzzy search", + matches!(self.selection, Selection::FuzzyOption), + ), + self.option_line( + self.options + .1 + .contains(SearchOptions::CASE_SENSITIVE), + "case sensitive", + matches!(self.selection, Selection::CaseOption), + ), + self.option_line( + self.options + .0 + .contains(SearchFields::MESSAGE_SUMMARY), + "summary", + matches!(self.selection, Selection::SummarySearch), + ), + self.option_line( + self.options.0.contains(SearchFields::MESSAGE_BODY), + "message body", + matches!( + self.selection, + Selection::MessageBodySearch ), - )]), + ), + self.option_line( + self.options.0.contains(SearchFields::FILENAMES), + "committed files", + matches!(self.selection, Selection::FilenameSearch), + ), + self.option_line( + self.options.0.contains(SearchFields::AUTHORS), + "authors", + matches!(self.selection, Selection::AuthorsSearch), + ), ] } @@ -315,7 +276,7 @@ impl LogSearchPopupPopup { } } - const fn move_selection(&mut self, arg: bool) { + fn move_selection(&mut self, arg: bool) { if arg { //up self.selection = match self.selection { @@ -513,9 +474,6 @@ impl LogSearchPopupPopup { self.execute_confirm(); } else if self.find_text.event(event)?.is_consumed() { self.validate_commit_sha(); - self.find_text.enabled( - !self.find_text.get_text().trim().is_empty(), - ); } } @@ -629,3 +587,58 @@ impl Component for LogSearchPopupPopup { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::Environment; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use ratatui::{backend::TestBackend, style::Color, Terminal}; + + fn key(code: KeyCode) -> Event { + Event::Key(KeyEvent::new(code, KeyModifiers::NONE)) + } + + #[test] + fn search_option_selection_uses_selection_background() { + let env = Environment::test_env(); + let mut popup = LogSearchPopupPopup::new(&env); + popup.open().unwrap(); + + // Move focus from the text field onto the first option. + popup.event(&key(KeyCode::Down)).unwrap(); + + let backend = TestBackend::new(80, 24); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + popup.draw(f, f.area()).unwrap(); + }) + .unwrap(); + + let buf = terminal.backend().buffer(); + // Find the "fuzzy search" row and assert it has selection_bg. + let mut found_selected = false; + let area = *buf.area(); + for y in area.top()..area.bottom() { + for x in area.left()..area.right() { + let cell = &buf[(x, y)]; + if cell.symbol() == "f" + && cell.fg == Color::White + && cell.bg == Color::Blue + { + // Selection highlight: command_fg on selection_bg + found_selected = true; + break; + } + } + if found_selected { + break; + } + } + assert!( + found_selected, + "focused search option should use selection background" + ); + } +}