fresh/view/controls/toggle/
render.rs1use ratatui::layout::Rect;
4use ratatui::style::Style;
5use ratatui::text::{Line, Span};
6use ratatui::widgets::Paragraph;
7use ratatui::Frame;
8
9use crate::primitives::display_width::str_width;
10
11use super::{FocusState, ToggleColors, ToggleLayout, ToggleState};
12
13fn pad_to_display_width(label: &str, width: u16) -> String {
14 let width = width as usize;
15 let padding = width.saturating_sub(str_width(label));
16 let mut padded = String::with_capacity(label.len() + padding);
17 padded.push_str(label);
18 padded.extend(std::iter::repeat_n(' ', padding));
19 padded
20}
21
22pub fn render_toggle(
33 frame: &mut Frame,
34 area: Rect,
35 state: &ToggleState,
36 colors: &ToggleColors,
37) -> ToggleLayout {
38 render_toggle_aligned(frame, area, state, colors, None)
39}
40
41pub fn render_toggle_aligned(
53 frame: &mut Frame,
54 area: Rect,
55 state: &ToggleState,
56 colors: &ToggleColors,
57 label_width: Option<u16>,
58) -> ToggleLayout {
59 if area.height == 0 || area.width < 4 {
60 return ToggleLayout {
61 checkbox_area: Rect::default(),
62 full_area: area,
63 };
64 }
65
66 let (bracket_color, check_color, label_color) = match state.focus {
72 FocusState::Normal => (colors.bracket, colors.checkmark, colors.label),
73 FocusState::Focused => (colors.focused_fg, colors.focused_fg, colors.focused_fg),
74 FocusState::Hovered => (colors.focused_fg, colors.focused_fg, colors.focused_fg),
75 FocusState::Disabled => (colors.disabled, colors.disabled, colors.disabled),
76 };
77
78 let label_display_width = str_width(&state.label) as u16;
80 let actual_label_width = label_width
81 .unwrap_or(label_display_width)
82 .max(label_display_width);
83 let padded_label = pad_to_display_width(&state.label, actual_label_width);
84
85 const CHIP_WIDTH: u16 = 3;
92
93 let line = if state.inherited {
94 Line::from(vec![
98 Span::styled(padded_label, Style::default().fg(label_color)),
99 Span::styled(": ", Style::default().fg(label_color)),
100 Span::styled("[", Style::default().fg(bracket_color)),
101 Span::styled("-", Style::default().fg(bracket_color)),
102 Span::styled("]", Style::default().fg(bracket_color)),
103 ])
104 } else if state.checked {
105 Line::from(vec![
106 Span::styled(padded_label, Style::default().fg(label_color)),
107 Span::styled(": ", Style::default().fg(label_color)),
108 Span::styled("[", Style::default().fg(bracket_color)),
109 Span::styled(
110 "v",
111 Style::default()
112 .fg(check_color)
113 .add_modifier(ratatui::style::Modifier::BOLD),
114 ),
115 Span::styled("]", Style::default().fg(bracket_color)),
116 ])
117 } else {
118 Line::from(vec![
119 Span::styled(padded_label, Style::default().fg(label_color)),
120 Span::styled(": ", Style::default().fg(label_color)),
121 Span::styled("[", Style::default().fg(bracket_color)),
122 Span::styled(" ", Style::default().fg(bracket_color)),
123 Span::styled("]", Style::default().fg(bracket_color)),
124 ])
125 };
126
127 let paragraph = Paragraph::new(line);
128 frame.render_widget(paragraph, area);
129
130 let label_overhead = actual_label_width.saturating_add(2);
132 let checkbox_start = area.x.saturating_add(label_overhead);
133 let chip_avail = area.width.saturating_sub(label_overhead.min(area.width));
134 let checkbox_area = Rect::new(checkbox_start, area.y, CHIP_WIDTH.min(chip_avail), 1);
135
136 let full_width = (actual_label_width + 2 + CHIP_WIDTH).min(area.width);
138 let full_area = Rect::new(area.x, area.y, full_width, 1);
139
140 ToggleLayout {
141 checkbox_area,
142 full_area,
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn pad_to_display_width_uses_terminal_columns() {
152 let padded = pad_to_display_width("你好", 6);
153
154 assert_eq!(str_width(&padded), 6);
155 assert_eq!(padded, "你好 ");
156 }
157}