1use crate::render::footer::{Hint, registry_hint_as, registry_hint_in};
5use datui_cli::keys::Context;
6use ratatui::buffer::Buffer;
7use ratatui::layout::Rect;
8use ratatui::style::{Modifier, Style};
9use ratatui::text::{Line, Span};
10use ratatui::widgets::{Paragraph, Widget};
11
12const GAP: u16 = 2;
14
15#[derive(Debug, Clone)]
17struct Chip {
18 hint: Hint,
19 weight: Option<i32>,
23}
24
25#[derive(Debug, Clone)]
31pub struct HintBar {
32 hints: Vec<Chip>,
33 screen: Option<(Context, Option<&'static str>)>,
34 key_style: Style,
35 label_style: Style,
36 accent_label_style: Style,
37}
38
39impl HintBar {
40 pub fn with_styles(key_style: Style, label_style: Style, accent_label_style: Style) -> Self {
42 Self {
43 hints: Vec::new(),
44 screen: None,
45 key_style,
46 label_style,
47 accent_label_style,
48 }
49 }
50
51 pub fn from_ctx(ctx: &crate::render::context::RenderContext) -> Self {
53 Self::with_styles(
54 Style::default()
55 .bg(ctx.keybind_hints)
56 .fg(ctx.text_inverse)
57 .add_modifier(Modifier::BOLD),
58 Style::default().fg(ctx.keybind_labels),
59 Style::default().fg(ctx.keybind_hints),
60 )
61 }
62
63 pub fn screen(mut self, context: Context) -> Self {
65 self.screen = Some((context, None));
66 self
67 }
68
69 pub fn group(mut self, group: &'static str) -> Self {
72 if let Some((_, in_group)) = self.screen.as_mut() {
73 *in_group = Some(group);
74 }
75 self
76 }
77
78 pub fn key(self, keys: &str) -> Self {
80 let (context, group) = self.screen.expect("a bar's screen before its keys");
81 self.push(registry_hint_in(context, group, keys))
82 }
83
84 pub fn key_as(self, keys: &str, label: &'static str) -> Self {
86 let (context, group) = self.screen.expect("a bar's screen before its keys");
87 self.push(registry_hint_as(context, group, keys, label))
88 }
89
90 pub fn push(mut self, hint: Hint) -> Self {
92 self.hints.push(Chip { hint, weight: None });
93 self
94 }
95
96 pub fn weight(mut self, weight: i32) -> Self {
98 if let Some(chip) = self.hints.last_mut() {
99 chip.weight = Some(weight);
100 }
101 self
102 }
103
104 pub fn accent(mut self, key: &str) -> Self {
106 for chip in &mut self.hints {
107 if chip.hint.key == key {
108 chip.hint.accented = true;
109 }
110 }
111 self
112 }
113
114 fn chip_width(chip: &Chip) -> u16 {
118 (crate::glyphs::display_width(&chip.hint.key) as u16 + 2)
119 + (crate::glyphs::display_width(&chip.hint.label) as u16 + 3)
120 }
121
122 fn kept(&self, width: u16, flush: bool) -> Vec<bool> {
126 let n = self.hints.len();
127 let mut keep = vec![true; n];
128 let weight = |i: usize| self.hints[i].weight.unwrap_or((n - i) as i32);
129 let budget = if flush {
130 width.saturating_add(GAP)
131 } else {
132 width
133 };
134 let mut used: u16 = self.hints.iter().map(Self::chip_width).sum();
135 while used > budget {
136 let Some(drop) = (0..n)
138 .filter(|&i| keep[i])
139 .min_by_key(|&i| (weight(i), std::cmp::Reverse(i)))
140 else {
141 break;
142 };
143 keep[drop] = false;
144 used -= Self::chip_width(&self.hints[drop]);
145 }
146 keep
147 }
148
149 fn used(&self, keep: &[bool]) -> u16 {
150 keep.iter()
151 .zip(&self.hints)
152 .filter(|(keep, _)| **keep)
153 .map(|(_, hint)| Self::chip_width(hint))
154 .sum()
155 }
156
157 #[cfg(test)]
159 pub fn width_in(&self, width: u16) -> u16 {
160 self.used(&self.kept(width, false))
161 }
162
163 pub fn flush_width_in(&self, width: u16) -> u16 {
166 self.used(&self.kept(width, true)).saturating_sub(GAP)
167 }
168
169 pub fn render_flush(&self, area: Rect, buf: &mut Buffer) {
172 self.draw(area, buf, true);
173 }
174
175 #[cfg(test)]
178 pub fn chips_in(&self, area: Rect) -> Vec<(Rect, &str)> {
179 self.chips(area, false)
180 }
181
182 fn chips(&self, area: Rect, flush: bool) -> Vec<(Rect, &str)> {
184 let mut x = area.x;
185 let mut chips = Vec::new();
186 for (hint, keep) in self.hints.iter().zip(self.kept(area.width, flush)) {
187 if !keep {
188 continue;
189 }
190 let width = Self::chip_width(hint);
191 let shown = (width - GAP).min(area.right().saturating_sub(x));
192 if shown > 0 {
193 chips.push((
194 Rect::new(x, area.y, shown, area.height.min(1)),
195 hint.hint.key.as_ref(),
196 ));
197 }
198 x = x.saturating_add(width);
199 }
200 chips
201 }
202
203 fn draw(&self, area: Rect, buf: &mut Buffer, flush: bool) {
204 for (rect, key) in self.chips(area, flush) {
207 if let Some(key) = crate::app::pointer::chip_key(key) {
208 crate::app::pointer::record(rect, crate::app::pointer::Hit::Chip(key));
209 }
210 }
211 let kept = self.kept(area.width, flush);
212 let mut spans = Vec::new();
213 for (chip, keep) in self.hints.iter().zip(kept) {
214 if !keep {
215 continue;
216 }
217 spans.push(Span::styled(format!(" {} ", chip.hint.key), self.key_style));
218 let style = if chip.hint.accented {
219 self.accent_label_style
220 } else {
221 self.label_style
222 };
223 spans.push(Span::styled(format!(" {} ", chip.hint.label), style));
224 }
225 Paragraph::new(Line::from(spans)).render(area, buf);
226 }
227}
228
229impl Widget for &HintBar {
230 fn render(self, area: Rect, buf: &mut Buffer) {
231 self.draw(area, buf, false);
232 }
233}
234
235#[cfg(test)]
236mod tests;