qframe/widgets/
key_hints.rs1use crate::geometry::{Rect, Size};
4use crate::keymap::Scope;
5use crate::text;
6use crate::widget::{MeasureCx, PaintCx, Widget};
7
8use super::cells;
9
10#[derive(Debug, Clone, Default)]
17pub struct KeyHints {
18 left: Vec<Hint>,
19 actions: Vec<(Scope, String, bool)>,
20}
21
22impl KeyHints {
23 #[must_use]
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 #[must_use]
31 pub fn hint(mut self, key: impl Into<String>, label: impl Into<String>) -> Self {
32 self.left.push((key.into(), label.into()));
33 self
34 }
35
36 #[must_use]
39 pub fn action(mut self, scope: Scope, action: impl Into<String>) -> Self {
40 self.actions.push((scope, action.into(), false));
41 self
42 }
43
44 #[must_use]
46 pub fn action_right(mut self, scope: Scope, action: impl Into<String>) -> Self {
47 self.actions.push((scope, action.into(), true));
48 self
49 }
50
51 fn resolved(&self, cx: &PaintCx<'_>) -> (Vec<Hint>, Vec<Hint>) {
52 let mut left = self.left.clone();
53 let mut right = Vec::new();
54 for (scope, action, on_right) in &self.actions {
55 let chords = cx.env().keymap().chords_for(*scope, action);
56 let Some(chord) = chords.first() else {
57 continue;
58 };
59 let label = cx.env().i18n().translate(&scope.label_key(action), &[]);
60 let hint = (chord.label(), label);
61 if *on_right {
62 right.push(hint);
63 } else {
64 left.push(hint);
65 }
66 }
67 (left, right)
68 }
69}
70
71type Hint = (String, String);
73
74const SPACING: u16 = 3;
76
77fn hint_width(hint: &Hint) -> u16 {
79 cells::sum([text::width(&hint.0), 2, 1, text::width(&hint.1)])
80}
81
82impl<Msg: 'static> Widget<Msg> for KeyHints {
83 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
84 Size::new(available.width, 1.min(available.height))
85 }
86
87 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
88 let bar = cx.style("key-hints", None, &[]);
89 let background = bar.text().bg.unwrap_or_else(|| cx.color("surface"));
90 cx.clear(area, background);
91 let inner = area.inset(bar.padding());
92 let key_style = cx.style("key-hint-key", None, &[]).text();
93 let label_style = cx.style("key-hint-label", None, &[]).text();
94 let (mut left, right) = self.resolved(cx);
95
96 let group_width = |hints: &[Hint]| -> u16 {
97 let count = u16::try_from(hints.len()).unwrap_or(u16::MAX);
98 let hints = cells::sum(hints.iter().map(hint_width));
99 hints.saturating_add(SPACING.saturating_mul(count.saturating_sub(1)))
100 };
101 let right_width = group_width(&right);
102 let separation = if right.is_empty() { 0 } else { SPACING };
103 let left_budget = inner.width.saturating_sub(right_width.saturating_add(separation));
104 while group_width(&left) > left_budget {
105 left.pop();
106 }
107 let draw = |cx: &mut PaintCx<'_>, mut x: i32, hints: &[Hint]| {
108 for (key, label) in hints {
109 let padded = format!(" {key} ");
110 x += i32::from(cx.text(x, inner.y, &padded, key_style, text::width(&padded))) + 1;
111 x += i32::from(cx.text(x, inner.y, label, label_style, text::width(label))) + i32::from(SPACING);
112 }
113 };
114 draw(cx, inner.x, &left);
115 draw(cx, inner.right() - i32::from(right_width), &right);
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::runtime::{App, Command, Harness};
123 use crate::widget::View;
124
125 struct Demo;
126
127 impl App for Demo {
128 type Msg = ();
129 fn update(&mut self, _: ()) -> Command<()> {
130 Command::none()
131 }
132 fn view(&self, ui: &mut View<'_, ()>) {
133 ui.add(
134 KeyHints::new()
135 .hint("↑↓", "move")
136 .action(Scope::Global, "focus-next")
137 .action_right(Scope::Global, "quit"),
138 )
139 .fill_width();
140 }
141 }
142
143 #[test]
144 fn draws_hints_from_keymap_and_drops_what_does_not_fit() {
145 let wide = Harness::new(Demo, 50, 1);
146 assert_eq!(wide.screen(), " ↑↓ move tab next ctrl q quit\n");
147 let narrow = Harness::new(Demo, 30, 1);
148 assert_eq!(narrow.screen(), " ↑↓ move ctrl q quit\n");
149 }
150
151 #[test]
152 fn labels_follow_the_language() {
153 let mut h = Harness::new(Demo, 50, 1);
154 h.set_locale("tr");
155 assert!(h.screen().contains("ctrl q çık"));
156 }
157
158 #[test]
159 fn hints_wider_than_any_screen_are_dropped_without_overflowing() {
160 struct Huge;
161
162 impl App for Huge {
163 type Msg = ();
164 fn update(&mut self, (): ()) -> Command<()> {
165 Command::none()
166 }
167 fn view(&self, ui: &mut View<'_, ()>) {
168 let long = "k".repeat(40_000);
169 ui.add(KeyHints::new().hint(long.clone(), long.clone()).hint(long, "move")).fill_width();
170 }
171 }
172
173 let h = Harness::new(Huge, 30, 1);
174 assert_eq!(h.screen(), "\n");
175 }
176}