Skip to main content

thedes_tui/
slidebar.rs

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}