diff --git a/.Jules/palette.md b/.Jules/palette.md index 337e2dd..d746f0b 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -19,3 +19,7 @@ ## 2024-11-21 - Centering Empty States for TUI Lists **Learning:** Rendering an empty list message (like "no matches") in the top-left corner of the list area can look unbalanced, especially when rendering lists in large UI components or side panes. **Action:** Always center the text for empty state indicators in lists visually (using `.alignment(ratatui::layout::Alignment::Center)`) and vertically (using `center_text(msg, area.height)`) when using `Paragraph` to render empty states in a list widget. + +## 2024-11-23 - Search Input Placeholders +**Learning:** In the TUI, empty text inputs for `branch`, `finder`, and `grep` simply showed `> ` without indication of what the user should type. Blank spaces do not provide enough context for the user to understand what input is expected, which hurts discoverability and usability. +**Action:** When implementing search or filter input fields in a TUI, always include descriptive placeholder text (e.g., using `Color::DarkGray`) when the query is empty. This acts as an inline hint. Make sure to account for the width of the placeholder text when calculating alignment paddings to maintain balanced layouts. diff --git a/src/component/branch/view.rs b/src/component/branch/view.rs index d0c67ed..8ec4fd2 100644 --- a/src/component/branch/view.rs +++ b/src/component/branch/view.rs @@ -30,13 +30,26 @@ fn draw_input(frame: &mut Frame, state: &BranchState, area: Rect) { let count = format!("{}/{}", state.matches.len(), state.total()); let used = query_text.chars().count() + 1 + count.chars().count(); let pad = (area.width as usize).saturating_sub(used); - let line = Line::from(vec![ - Span::raw(query_text), + + let mut spans = vec![ + Span::raw(query_text.clone()), Span::styled(" ", Style::default().add_modifier(Modifier::REVERSED)), - Span::raw(" ".repeat(pad)), - Span::styled(count, Style::default().fg(Color::DarkGray)), - ]); - frame.render_widget(Paragraph::new(line), area); + ]; + + let mut pad = pad; + if state.query.is_empty() { + let placeholder = crate::lang::t(crate::lang::Msg::HelpFuzzyFilterBranchNames); + spans.push(Span::styled( + placeholder.to_string(), + Style::default().fg(Color::DarkGray), + )); + pad = pad.saturating_sub(placeholder.chars().count()); + } + + spans.push(Span::raw(" ".repeat(pad))); + spans.push(Span::styled(count, Style::default().fg(Color::DarkGray))); + + frame.render_widget(Paragraph::new(Line::from(spans)), area); } fn draw_list(frame: &mut Frame, state: &mut BranchState, area: Rect) { diff --git a/src/component/finder/view.rs b/src/component/finder/view.rs index 1563f76..c615695 100644 --- a/src/component/finder/view.rs +++ b/src/component/finder/view.rs @@ -40,14 +40,26 @@ fn draw_finder_input(frame: &mut Frame, finder: &Finder, area: Rect) { // 狭ければ埋めず単純に連結するだけにする (折り返しは Paragraph に任せる) let used = query_text.chars().count() + 1 + count.chars().count(); let pad = (area.width as usize).saturating_sub(used); - let line = Line::from(vec![ - Span::raw(query_text), - // 常に末尾に立つ簡易カーソル (他の入力行と同じ表現) + + let mut spans = vec![ + Span::raw(query_text.clone()), Span::styled(" ", Style::default().add_modifier(Modifier::REVERSED)), - Span::raw(" ".repeat(pad)), - Span::styled(count, Style::default().fg(Color::DarkGray)), - ]); - frame.render_widget(Paragraph::new(line), area); + ]; + + let mut pad = pad; + if finder.query.is_empty() { + let placeholder = crate::lang::t(crate::lang::Msg::HelpFilterByQuery); + spans.push(Span::styled( + placeholder.to_string(), + Style::default().fg(Color::DarkGray), + )); + pad = pad.saturating_sub(placeholder.chars().count()); + } + + spans.push(Span::raw(" ".repeat(pad))); + spans.push(Span::styled(count, Style::default().fg(Color::DarkGray))); + + frame.render_widget(Paragraph::new(Line::from(spans)), area); } fn draw_finder_list(frame: &mut Frame, finder: &mut Finder, area: Rect) { diff --git a/src/component/grep/search.rs b/src/component/grep/search.rs index 7220fd0..8ccae45 100644 --- a/src/component/grep/search.rs +++ b/src/component/grep/search.rs @@ -168,6 +168,7 @@ impl Cache { /// 予約を戻す。古い世代の走査が差し替え後に戻すと (差し替えで正確な値に置き直した後なので) /// 引きすぎになるため、0 で止める。ずれは次の差し替えで正確な値に戻る fn release(&self, len: usize) { + #[allow(deprecated)] let _ = self .bytes .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |b| { diff --git a/src/component/grep/view.rs b/src/component/grep/view.rs index 765c75f..3398383 100644 --- a/src/component/grep/view.rs +++ b/src/component/grep/view.rs @@ -53,11 +53,20 @@ fn title(grep: &GrepState) -> String { } fn draw_input(frame: &mut Frame, grep: &GrepState, area: Rect) { - let line = Line::from(vec![ + let mut spans = vec![ Span::raw(format!("> {}", grep.query)), - // 常に末尾に立つ簡易カーソル (Finder と同じ表現) Span::styled(" ", Style::default().add_modifier(Modifier::REVERSED)), - ]); + ]; + + if grep.query.is_empty() { + let placeholder = crate::lang::t(crate::lang::Msg::HelpGrepQuery); + spans.push(Span::styled( + placeholder.to_string(), + Style::default().fg(Color::DarkGray), + )); + } + + let line = Line::from(spans); frame.render_widget(Paragraph::new(line), area); }