1use std::iter;
2
3use thedes_tui_core::{
4 App,
5 event::{Event, Key, KeyEvent},
6 geometry::Coord,
7 mutation::Set,
8 screen::{self, FlushError},
9};
10
11pub use style::Style;
12use thiserror::Error;
13
14use crate::text;
15
16mod style;
17
18pub fn default_key_bindings() -> KeyBindingMap {
19 KeyBindingMap::new()
20 .with(Key::Enter, Command::Back)
21 .with(Key::Esc, Command::Back)
22 .with(Key::Right, Command::Increase(1))
23 .with(Key::Left, Command::Decrease(1))
24 .with(
25 KeyEvent {
26 ctrl: true,
27 alt: false,
28 shift: false,
29 main_key: Key::Right,
30 },
31 Command::Increase(3),
32 )
33 .with(
34 KeyEvent {
35 ctrl: true,
36 alt: false,
37 shift: false,
38 main_key: Key::Left,
39 },
40 Command::Decrease(3),
41 )
42}
43
44pub type KeyBindingMap = crate::key_bindings::KeyBindingMap<Command>;
45
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum Command {
48 Back,
49 Increase(Coord),
50 Decrease(Coord),
51}
52
53#[derive(Debug, Error)]
54pub enum Error {
55 #[error("insufficient width")]
56 InsufficientWidth,
57 #[error("Failed to flush tiles to canvas")]
58 CanvasFlush(#[from] FlushError),
59 #[error("Failed to render text")]
60 RenderText(#[from] text::Error),
61 #[error("Menu was cancelled")]
62 Cancelled,
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct Config {
67 pub ui_size: Coord,
68 pub logical_size: Coord,
69 pub logical_current: Coord,
70}
71
72#[derive(Debug, Clone)]
73pub struct Slidebar {
74 style: Style,
75 title: String,
76 message: Option<String>,
77 ui_size: Coord,
78 logical_size: Coord,
79 ui_current: Coord,
80 key_bindings: KeyBindingMap,
81}
82
83impl Slidebar {
84 pub fn new(title: impl AsRef<str>, config: Config) -> Self {
85 let denom = config.logical_current * (config.ui_size - 1)
86 + config.logical_size / 2;
87 let ui_current = denom / (config.logical_size - 1);
88 Self {
89 style: Style::default(),
90 title: title.as_ref().to_owned(),
91 ui_size: config.ui_size,
92 logical_size: config.logical_size,
93 ui_current,
94 key_bindings: default_key_bindings(),
95 message: None,
96 }
97 }
98
99 pub fn with_title(mut self, title: &str) -> Self {
100 self.set_title(title);
101 self
102 }
103
104 pub fn set_title(&mut self, title: &str) -> &mut Self {
105 self.title = title.to_owned();
106 self
107 }
108
109 pub fn title(&self) -> &str {
110 &self.title
111 }
112
113 pub fn with_message(mut self, message: &str) -> Self {
114 self.set_message(message);
115 self
116 }
117
118 pub fn set_message(&mut self, message: &str) -> &mut Self {
119 self.message = Some(message.to_owned());
120 self
121 }
122
123 pub fn message(&self) -> Option<&str> {
124 self.message.as_deref()
125 }
126
127 pub fn with_style(self, style: Style) -> Self {
128 Self { style, ..self }
129 }
130
131 pub fn style(&self) -> &Style {
132 &self.style
133 }
134
135 pub fn with_key_bindings(self, key_bindings: KeyBindingMap) -> Self {
136 Self { key_bindings, ..self }
137 }
138
139 pub fn key_bindings(&self) -> &KeyBindingMap {
140 &self.key_bindings
141 }
142
143 pub fn ui_size(&self) -> Coord {
144 self.ui_size
145 }
146
147 pub fn logical_size(&self) -> Coord {
148 self.logical_size
149 }
150
151 pub fn ui_current(&self) -> Coord {
152 self.ui_current
153 }
154
155 pub fn logical_current(&self) -> Coord {
156 let denom =
157 self.ui_current() * (self.logical_size() - 1) + self.ui_size() / 2;
158 denom / (self.ui_size() - 1)
159 }
160
161 pub fn set_ui_size(&mut self, value: Coord) {
162 self.ui_size = value;
163 self.set_ui_current(self.ui_current());
164 }
165
166 pub fn set_ui_current(&mut self, value: Coord) {
167 self.ui_current = value.min(self.ui_size().saturating_sub(1));
168 }
169
170 pub fn run_command<F>(
171 &mut self,
172 app: &mut App,
173 cmd: Command,
174 mut on_change: F,
175 ) -> Result<bool, Error>
176 where
177 F: FnMut(&mut App, Coord),
178 {
179 match cmd {
180 Command::Back => return Ok(false),
181 Command::Increase(amount) => {
182 self.set_ui_current(self.ui_current().saturating_add(amount));
183 on_change(app, self.logical_current());
184 },
185 Command::Decrease(amount) => {
186 self.set_ui_current(self.ui_current().saturating_sub(amount));
187 on_change(app, self.logical_current());
188 },
189 }
190 Ok(true)
191 }
192
193 pub async fn run<F>(
194 &mut self,
195 app: &mut App,
196 mut on_change: F,
197 ) -> Result<(), Error>
198 where
199 F: FnMut(&mut App, Coord),
200 {
201 self.available_width(app)?;
202
203 while self.handle_input(app, &mut on_change)? {
204 self.render(app)?;
205 tokio::select! {
206 _ = app.tick_session.tick() => (),
207 _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
208 }
209 }
210 Ok(())
211 }
212
213 fn handle_input<F>(
214 &mut self,
215 app: &mut App,
216 on_change: &mut F,
217 ) -> Result<bool, Error>
218 where
219 F: FnMut(&mut App, Coord),
220 {
221 let Ok(events) = app.events.read_until_now() else {
222 Err(Error::Cancelled)?
223 };
224 let mut events = Vec::from_iter(events).into_iter();
225 let mut should_continue = true;
226 while let Some(event) = events.next().filter(|_| should_continue) {
227 let Event::Key(key) = event else { continue };
228 let Some(&command) = self.key_bindings.command_for(key) else {
229 continue;
230 };
231 should_continue =
232 self.run_command(app, command, &mut *on_change)?;
233 }
234 Ok(should_continue)
235 }
236
237 fn available_width(&self, app: &App) -> Result<Coord, Error> {
238 let size = app.canvas.size();
239 size.y
240 .checked_sub(self.style.left_margin())
241 .and_then(|y| y.checked_sub(self.style.right_margin()))
242 .and_then(|y| {
243 Coord::try_from(
244 app.grapheme_registry.len_of(self.style.left_arrow()),
245 )
246 .ok()
247 .and_then(|len| y.checked_sub(len))
248 })
249 .and_then(|y| {
250 Coord::try_from(
251 app.grapheme_registry.len_of(self.style.right_arrow()),
252 )
253 .ok()
254 .and_then(|len| y.checked_sub(len))
255 })
256 .ok_or(Error::InsufficientWidth)
257 }
258
259 fn render(&mut self, app: &mut App) -> Result<(), Error> {
260 app.canvas
261 .queue([screen::Command::ClearScreen(self.style().background())]);
262
263 let mut height = self.style().top_margin();
264 self.render_title(app, &mut height)?;
265
266 self.render_slidebar(app, &mut height)?;
267 self.render_message(app, &mut height)?;
268 self.render_back(app, &mut height)?;
269
270 app.canvas.flush()?;
271
272 Ok(())
273 }
274
275 fn render_title(
276 &mut self,
277 app: &mut App,
278 height: &mut Coord,
279 ) -> Result<(), Error> {
280 *height = text::styled(
281 app,
282 self.title(),
283 &text::Style::new_with_colors(Set(self.style().title_colors()))
284 .with_align(1, 2)
285 .with_top_margin(*height)
286 .with_left_margin(self.style().left_margin())
287 .with_right_margin(self.style().right_margin()),
288 )?;
289 *height += self.style().title_slidebar_padding();
290 Ok(())
291 }
292
293 fn render_slidebar(
294 &mut self,
295 app: &mut App,
296 height: &mut Coord,
297 ) -> Result<(), Error> {
298 let mut bar = self.style().left_arrow().to_owned();
299 let slide_handle_len =
300 app.grapheme_registry.len_of(self.style().slide_handle());
301 let left_count =
302 usize::from(self.ui_current() + 1).saturating_sub(slide_handle_len);
303 let right_count = usize::from(self.ui_size() - self.ui_current())
304 .saturating_sub(slide_handle_len);
305 let left_bar_ch = app.grapheme_registry.lookup(
306 self.style().left_bar_ch(),
307 |result| match result {
308 Ok(chars) => String::from_iter(chars),
309 Err(_) => String::new(),
310 },
311 );
312 let right_bar_ch = app.grapheme_registry.lookup(
313 self.style().right_bar_ch(),
314 |result| match result {
315 Ok(chars) => String::from_iter(chars),
316 Err(_) => String::new(),
317 },
318 );
319 bar.extend(
320 iter::repeat_n(&left_bar_ch[..], left_count).flat_map(str::chars),
321 );
322 bar.push_str(self.style().slide_handle());
323 bar.extend(
324 iter::repeat_n(&right_bar_ch[..], right_count).flat_map(str::chars),
325 );
326 text::styled(
327 app,
328 &bar,
329 &text::Style::new_with_colors(Set(self.style().bar_colors()))
330 .with_align(1, 2)
331 .with_top_margin(*height)
332 .with_left_margin(self.style().left_margin())
333 .with_right_margin(self.style().right_margin()),
334 )?;
335 *height += 1;
336 Ok(())
337 }
338
339 fn render_message(
340 &mut self,
341 app: &mut App,
342 height: &mut Coord,
343 ) -> Result<(), Error> {
344 *height += self.style().slidebar_message_padding();
345 if let Some(message) = self.message() {
346 text::styled(
347 app,
348 message,
349 &text::Style::new_with_colors(Set(self
350 .style()
351 .message_colors()))
352 .with_align(1, 2)
353 .with_top_margin(*height),
354 )?;
355 *height += 1;
356 }
357 Ok(())
358 }
359
360 fn render_back(
361 &mut self,
362 app: &mut App,
363 height: &mut Coord,
364 ) -> Result<(), Error> {
365 *height += self.style().message_back_padding();
366 text::styled(
367 app,
368 &self.style().back_label(),
369 &text::Style::new_with_colors(Set(self.style().back_colors()))
370 .with_align(1, 2)
371 .with_top_margin(*height),
372 )?;
373 *height += 1;
374 Ok(())
375 }
376}