Skip to content

Commit 34d0bdb

Browse files
committed
fix(ui): restore log search option selection highlight
Option rows used theme.text(focused, false), which only dimmed non-focused lines and never applied selection_bg. TextInputComponent::enabled also stopped updating textarea styles after the tui-textarea migration, so the cursor stayed visible when navigating checkboxes. Use theme.text(true, selected) for option focus, refresh textarea style/cursor on enabled(), and keep jump-to-SHA field focused (invalid SHA still uses block danger styling).
1 parent bc086cf commit 34d0bdb

2 files changed

Lines changed: 169 additions & 110 deletions

File tree

src/components/textinput.rs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,34 @@ impl TextInputComponent {
131131
self.embed = true;
132132
}
133133

134-
///
135-
pub const fn enabled(&mut self, enable: bool) {
134+
/// Focus/unfocus the text input visually (style + cursor).
135+
/// Used by popups that share the field with other selectable rows.
136+
pub fn enabled(&mut self, enable: bool) {
136137
self.selected = Some(enable);
138+
self.apply_enabled_style();
139+
}
140+
141+
fn is_enabled(&self) -> bool {
142+
self.selected.unwrap_or(true)
143+
}
144+
145+
fn apply_enabled_style(&mut self) {
146+
let enabled = self.is_enabled();
147+
let Some(ta) = self.textarea.as_mut() else {
148+
return;
149+
};
150+
151+
let style = self.theme.text(enabled, false);
152+
ta.set_style(style);
153+
ta.set_placeholder_style(style);
154+
// Hide the block cursor when another control has focus.
155+
// Default REVERSED cursor is what ratatui-textarea uses when focused.
156+
ta.set_cursor_style(if enabled {
157+
ratatui::style::Style::default()
158+
.add_modifier(ratatui::style::Modifier::REVERSED)
159+
} else {
160+
ratatui::style::Style::default()
161+
});
137162
}
138163

139164
fn show_inner_textarea(&mut self) {
@@ -156,13 +181,6 @@ impl TextInputComponent {
156181
text_area
157182
.set_cursor_line_style(self.theme.text(true, false));
158183
text_area.set_placeholder_text(self.default_msg.clone());
159-
text_area.set_placeholder_style(
160-
self.theme
161-
.text(self.selected.unwrap_or_default(), false),
162-
);
163-
text_area.set_style(
164-
self.theme.text(self.selected.unwrap_or(true), false),
165-
);
166184

167185
if !self.embed {
168186
text_area.set_block(
@@ -179,6 +197,8 @@ impl TextInputComponent {
179197
}
180198
text_area
181199
});
200+
// Apply focus style/cursor after the widget exists (and on rebuilds).
201+
self.apply_enabled_style();
182202
}
183203

184204
/// Set the `msg`.
@@ -887,4 +907,30 @@ mod tests {
887907
assert_eq!(ta.cursor(), save_cursor);
888908
}
889909
}
910+
911+
#[test]
912+
fn test_enabled_toggles_cursor_visibility() {
913+
use ratatui::style::Modifier;
914+
915+
let env = Environment::test_env();
916+
let mut comp = TextInputComponent::new(&env, "", "", false);
917+
comp.show_inner_textarea();
918+
comp.enabled(true);
919+
assert!(comp
920+
.textarea
921+
.as_ref()
922+
.unwrap()
923+
.cursor_style()
924+
.add_modifier
925+
.contains(Modifier::REVERSED));
926+
927+
comp.enabled(false);
928+
assert!(!comp
929+
.textarea
930+
.as_ref()
931+
.unwrap()
932+
.cursor_style()
933+
.add_modifier
934+
.contains(Modifier::REVERSED));
935+
}
890936
}

src/popups/log_search.rs

Lines changed: 114 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ impl LogSearchPopupPopup {
107107
self.mode = PopupMode::JumpCommitSha;
108108
self.jump_commit_id = None;
109109
self.find_text.set_default_msg("commit sha".into());
110-
self.find_text.enabled(false);
110+
// Field stays focused; invalid SHA is shown via block style.
111+
self.find_text.enabled(true);
111112
self.selection = Selection::EnterText;
112113
}
113114
}
@@ -164,106 +165,66 @@ impl LogSearchPopupPopup {
164165
}
165166
}
166167

167-
fn get_text_options(&self) -> Vec<Line<'_>> {
168-
let x_summary =
169-
if self.options.0.contains(SearchFields::MESSAGE_SUMMARY)
170-
{
171-
"X"
172-
} else {
173-
" "
174-
};
175-
176-
let x_body =
177-
if self.options.0.contains(SearchFields::MESSAGE_BODY) {
178-
"X"
179-
} else {
180-
" "
181-
};
182-
183-
let x_files =
184-
if self.options.0.contains(SearchFields::FILENAMES) {
185-
"X"
186-
} else {
187-
" "
188-
};
189-
190-
let x_authors =
191-
if self.options.0.contains(SearchFields::AUTHORS) {
192-
"X"
193-
} else {
194-
" "
195-
};
196-
197-
let x_opt_fuzzy =
198-
if self.options.1.contains(SearchOptions::FUZZY_SEARCH) {
199-
"X"
200-
} else {
201-
" "
202-
};
168+
fn option_mark(on: bool) -> &'static str {
169+
if on {
170+
"X"
171+
} else {
172+
" "
173+
}
174+
}
203175

204-
let x_opt_casesensitive =
205-
if self.options.1.contains(SearchOptions::CASE_SENSITIVE)
206-
{
207-
"X"
208-
} else {
209-
" "
210-
};
176+
fn option_line(
177+
&self,
178+
checked: bool,
179+
label: &str,
180+
selected: bool,
181+
) -> Line<'_> {
182+
Line::from(vec![Span::styled(
183+
format!("[{}] {label}", Self::option_mark(checked)),
184+
// enabled=true keeps normal fg; selected applies selection_bg
185+
self.theme.text(true, selected),
186+
)])
187+
}
211188

189+
fn get_text_options(&self) -> Vec<Line<'_>> {
212190
vec![
213-
Line::from(vec![Span::styled(
214-
format!("[{x_opt_fuzzy}] fuzzy search"),
215-
self.theme.text(
216-
matches!(self.selection, Selection::FuzzyOption),
217-
false,
218-
),
219-
)]),
220-
Line::from(vec![Span::styled(
221-
format!("[{x_opt_casesensitive}] case sensitive"),
222-
self.theme.text(
223-
matches!(self.selection, Selection::CaseOption),
224-
false,
225-
),
226-
)]),
227-
Line::from(vec![Span::styled(
228-
format!("[{x_summary}] summary"),
229-
self.theme.text(
230-
matches!(
231-
self.selection,
232-
Selection::SummarySearch
233-
),
234-
false,
235-
),
236-
)]),
237-
Line::from(vec![Span::styled(
238-
format!("[{x_body}] message body"),
239-
self.theme.text(
240-
matches!(
241-
self.selection,
242-
Selection::MessageBodySearch
243-
),
244-
false,
245-
),
246-
)]),
247-
Line::from(vec![Span::styled(
248-
format!("[{x_files}] committed files"),
249-
self.theme.text(
250-
matches!(
251-
self.selection,
252-
Selection::FilenameSearch
253-
),
254-
false,
255-
),
256-
)]),
257-
Line::from(vec![Span::styled(
258-
format!("[{x_authors}] authors"),
259-
self.theme.text(
260-
matches!(
261-
self.selection,
262-
Selection::AuthorsSearch
263-
),
264-
false,
191+
self.option_line(
192+
self.options.1.contains(SearchOptions::FUZZY_SEARCH),
193+
"fuzzy search",
194+
matches!(self.selection, Selection::FuzzyOption),
195+
),
196+
self.option_line(
197+
self.options
198+
.1
199+
.contains(SearchOptions::CASE_SENSITIVE),
200+
"case sensitive",
201+
matches!(self.selection, Selection::CaseOption),
202+
),
203+
self.option_line(
204+
self.options
205+
.0
206+
.contains(SearchFields::MESSAGE_SUMMARY),
207+
"summary",
208+
matches!(self.selection, Selection::SummarySearch),
209+
),
210+
self.option_line(
211+
self.options.0.contains(SearchFields::MESSAGE_BODY),
212+
"message body",
213+
matches!(
214+
self.selection,
215+
Selection::MessageBodySearch
265216
),
266-
)]),
217+
),
218+
self.option_line(
219+
self.options.0.contains(SearchFields::FILENAMES),
220+
"committed files",
221+
matches!(self.selection, Selection::FilenameSearch),
222+
),
223+
self.option_line(
224+
self.options.0.contains(SearchFields::AUTHORS),
225+
"authors",
226+
matches!(self.selection, Selection::AuthorsSearch),
227+
),
267228
]
268229
}
269230

@@ -315,7 +276,7 @@ impl LogSearchPopupPopup {
315276
}
316277
}
317278

318-
const fn move_selection(&mut self, arg: bool) {
279+
fn move_selection(&mut self, arg: bool) {
319280
if arg {
320281
//up
321282
self.selection = match self.selection {
@@ -513,9 +474,6 @@ impl LogSearchPopupPopup {
513474
self.execute_confirm();
514475
} else if self.find_text.event(event)?.is_consumed() {
515476
self.validate_commit_sha();
516-
self.find_text.enabled(
517-
!self.find_text.get_text().trim().is_empty(),
518-
);
519477
}
520478
}
521479

@@ -629,3 +587,58 @@ impl Component for LogSearchPopupPopup {
629587
Ok(())
630588
}
631589
}
590+
591+
#[cfg(test)]
592+
mod tests {
593+
use super::*;
594+
use crate::app::Environment;
595+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
596+
use ratatui::{backend::TestBackend, style::Color, Terminal};
597+
598+
fn key(code: KeyCode) -> Event {
599+
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
600+
}
601+
602+
#[test]
603+
fn search_option_selection_uses_selection_background() {
604+
let env = Environment::test_env();
605+
let mut popup = LogSearchPopupPopup::new(&env);
606+
popup.open().unwrap();
607+
608+
// Move focus from the text field onto the first option.
609+
popup.event(&key(KeyCode::Down)).unwrap();
610+
611+
let backend = TestBackend::new(80, 24);
612+
let mut terminal = Terminal::new(backend).unwrap();
613+
terminal
614+
.draw(|f| {
615+
popup.draw(f, f.area()).unwrap();
616+
})
617+
.unwrap();
618+
619+
let buf = terminal.backend().buffer();
620+
// Find the "fuzzy search" row and assert it has selection_bg.
621+
let mut found_selected = false;
622+
let area = *buf.area();
623+
for y in area.top()..area.bottom() {
624+
for x in area.left()..area.right() {
625+
let cell = &buf[(x, y)];
626+
if cell.symbol() == "f"
627+
&& cell.fg == Color::White
628+
&& cell.bg == Color::Blue
629+
{
630+
// Selection highlight: command_fg on selection_bg
631+
found_selected = true;
632+
break;
633+
}
634+
}
635+
if found_selected {
636+
break;
637+
}
638+
}
639+
assert!(
640+
found_selected,
641+
"focused search option should use selection background"
642+
);
643+
}
644+
}

0 commit comments

Comments
 (0)