tui_kit/block.rs
1use ratatui::{
2 layout::Rect,
3 text::{Line, Span},
4 widgets::{Block, Borders, Scrollbar, ScrollbarOrientation, ScrollbarState},
5 Frame,
6};
7
8use crate::Theme;
9
10/// Creates a bordered [`Block`] for a main-content panel.
11///
12/// - `focused = true` → uses [`Theme::border_focused`] (accent color, e.g. Green+Bold).
13/// - `focused = false` → uses [`Theme::border_unfocused`] (e.g. White).
14///
15/// The title is any `Line<'static>` — use [`widget_title`] or [`crate::tabs::tab_line`]
16/// to build it, or pass `Line::from("My Panel")` for a plain string.
17///
18/// For a simple focusable widget with an optional digit shortcut, prefer
19/// [`focusable_block`] which builds both the title and the block in one call.
20pub fn panel_block(title: Line<'static>, focused: bool, theme: &Theme) -> Block<'static> {
21 let border_style = if focused {
22 theme.border_focused
23 } else {
24 theme.border_unfocused
25 };
26 Block::default()
27 .borders(Borders::ALL)
28 .border_style(border_style)
29 .title(title)
30}
31
32/// Creates a bordered [`Block`] for a floating popup.
33///
34/// Always uses [`Theme::border_popup`] regardless of focus state,
35/// since a visible popup is by definition the active element.
36pub fn popup_block(title: Line<'static>, theme: &Theme) -> Block<'static> {
37 Block::default()
38 .borders(Borders::ALL)
39 .border_style(theme.border_popup)
40 .title(title)
41}
42
43/// Convenience wrapper: builds a [`widget_title`] and a [`panel_block`] in one call.
44///
45/// Use this for any widget that is focusable and optionally has a digit shortcut.
46/// The border color and the digit indicator always stay in sync.
47///
48/// ```ignore
49/// let block = focusable_block("Status Log", Some(2), focused, &theme);
50/// ```
51pub fn focusable_block(title: &str, shortcut: Option<u8>, focused: bool, theme: &Theme) -> Block<'static> {
52 let title_line = widget_title(title, shortcut, focused, theme);
53 panel_block(title_line, focused, theme)
54}
55
56/// Builds a widget title [`Line`] with an optional keyboard-shortcut digit indicator.
57///
58/// The digit and `─` separator blend into the border line (same color), producing:
59///
60/// ```text
61/// ┌─ 1 ─ Favorites ────────────────────────────────────────────┐
62/// ```
63///
64/// - `shortcut = Some(1)` → ` [1] ─ Label `
65/// - `shortcut = None` → ` Label `
66///
67/// `active` controls the label style ([`Theme::tab_active`] vs [`Theme::tab_inactive`])
68/// and the digit+separator color ([`Theme::border_focused`] vs [`Theme::border_unfocused`]),
69/// so they always match the surrounding border.
70///
71/// # Example
72///
73/// ```ignore
74/// // Simple panel title, always active
75/// let title = widget_title("Now Playing", None, true, &theme);
76///
77/// // Panel with shortcut indicator, focus-aware
78/// let title = widget_title("Favorites", Some(1), focused, &theme);
79/// ```
80/// Render a native ratatui vertical scrollbar over the right border of `area`.
81///
82/// Pass the **outer** block area (including borders). The scrollbar overlaps the
83/// right border column, which is the standard ratatui pattern.
84///
85/// - `total` — total number of content rows / lines.
86/// - `position` — index of the first visible row (the scroll offset).
87///
88/// Does nothing when all content fits in the visible area.
89pub fn render_scrollbar(f: &mut Frame, area: Rect, total: usize, position: usize) {
90 let visible = area.height.saturating_sub(2) as usize; // subtract top+bottom borders
91 if total <= visible {
92 return;
93 }
94 let mut state = ScrollbarState::new(total).position(position);
95 f.render_stateful_widget(
96 Scrollbar::new(ScrollbarOrientation::VerticalRight),
97 area,
98 &mut state,
99 );
100}
101
102pub fn widget_title(label: &str, shortcut: Option<u8>, active: bool, theme: &Theme) -> Line<'static> {
103 let label_style = if active { theme.tab_active } else { theme.tab_inactive };
104 let border_style = if active { theme.border_focused } else { theme.border_unfocused };
105
106 match shortcut {
107 Some(n) => Line::from(vec![
108 Span::styled(format!("[{}]\u{2500} ", n), border_style),
109 Span::styled(label.to_string(), label_style),
110 Span::raw(" "),
111 ]),
112 None => Line::from(vec![
113 Span::raw(" "),
114 Span::styled(label.to_string(), label_style),
115 Span::raw(" "),
116 ]),
117 }
118}