thedes_tui/
info.rs

1pub use style::Style;
2use thedes_tui_core::{
3    App,
4    event::{Event, Key},
5    geometry::Coord,
6    mutation::Set,
7    screen::{self, FlushError},
8};
9use thiserror::Error;
10use unicode_segmentation::UnicodeSegmentation;
11
12use crate::{
13    cancellability::{Cancellation, NonCancellable},
14    text,
15};
16
17mod style;
18
19pub type KeyBindingMap = crate::key_bindings::KeyBindingMap<Command>;
20
21pub fn default_key_bindings() -> KeyBindingMap {
22    KeyBindingMap::new()
23        .with(Key::Enter, Command::Confirm)
24        .with(Key::Esc, Command::ConfirmCancel)
25        .with(Key::Up, Command::ItemAbove)
26        .with(Key::Down, Command::ItemBelow)
27}
28
29#[derive(Debug, Error)]
30pub enum Error {
31    #[error("Failed to render text")]
32    RenderText(
33        #[from]
34        #[source]
35        text::Error,
36    ),
37    #[error("Failed to flush tiles to canvas")]
38    CanvasFlush(
39        #[from]
40        #[source]
41        FlushError,
42    ),
43    #[error("Info dialog was cancelled")]
44    Cancelled,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub enum Command {
49    Confirm,
50    ConfirmCancel,
51    ConfirmOk,
52    ItemAbove,
53    ItemBelow,
54    SelectCancel,
55    SelectOk,
56}
57
58#[derive(Debug, Clone)]
59pub struct Info<C = NonCancellable> {
60    title: String,
61    message: String,
62    cancellation: C,
63    style: Style,
64    key_bindings: KeyBindingMap,
65}
66
67impl Info {
68    pub fn new(title: impl AsRef<str>, message: impl AsRef<str>) -> Self {
69        Self::from_cancellation(title, message, NonCancellable)
70    }
71}
72
73impl<C> Info<C>
74where
75    C: Cancellation<String>,
76{
77    pub fn from_cancellation(
78        title: impl AsRef<str>,
79        message: impl AsRef<str>,
80        cancellation: C,
81    ) -> Self {
82        Self {
83            title: title.as_ref().to_owned(),
84            message: message.as_ref().to_owned(),
85            cancellation,
86            key_bindings: default_key_bindings(),
87            style: Style::default(),
88        }
89    }
90
91    pub fn with_title(mut self, title: &str) -> Self {
92        self.set_title(title);
93        self
94    }
95
96    pub fn set_title(&mut self, title: &str) -> &mut Self {
97        self.title = title.to_owned();
98        self
99    }
100
101    pub fn with_message(mut self, message: &str) -> Self {
102        self.set_message(message);
103        self
104    }
105
106    pub fn set_message(&mut self, message: &str) -> &mut Self {
107        self.message = message.to_owned();
108        self
109    }
110
111    pub fn with_cancellation(mut self, cancellation: C) -> Self {
112        self.set_cancellation(cancellation);
113        self
114    }
115
116    pub fn set_cancellation(&mut self, cancellation: C) -> &mut Self {
117        self.cancellation = cancellation;
118        self
119    }
120
121    pub fn with_style(mut self, style: Style) -> Self {
122        self.set_style(style);
123        self
124    }
125
126    pub fn set_style(&mut self, style: Style) -> &mut Self {
127        self.style = style;
128        self
129    }
130
131    pub fn style(&self) -> &Style {
132        &self.style
133    }
134
135    pub fn key_bindings(&self) -> &KeyBindingMap {
136        &self.key_bindings
137    }
138
139    pub fn cancellation(&self) -> &C {
140        &self.cancellation
141    }
142
143    pub fn is_cancellable(&self) -> bool {
144        self.cancellation().is_cancellable()
145    }
146
147    pub fn is_cancelling(&self) -> bool {
148        self.cancellation().is_cancelling()
149    }
150
151    pub fn set_cancelling(&mut self, is_it: bool) {
152        self.cancellation.set_cancelling(is_it);
153    }
154
155    pub fn title(&self) -> &str {
156        &self.title
157    }
158
159    pub fn message(&self) -> &str {
160        &self.message
161    }
162
163    pub fn run_command(&mut self, cmd: Command) -> Result<bool, Error> {
164        match cmd {
165            Command::Confirm => {
166                return Ok(false);
167            },
168            Command::ConfirmCancel => {
169                if self.is_cancellable() {
170                    self.set_cancelling(true);
171                    return Ok(false);
172                }
173            },
174            Command::ConfirmOk => {
175                self.set_cancelling(false);
176                return Ok(false);
177            },
178            Command::ItemAbove | Command::SelectOk => {
179                self.set_cancelling(false);
180            },
181            Command::ItemBelow | Command::SelectCancel => {
182                self.set_cancelling(true);
183            },
184        }
185        Ok(true)
186    }
187
188    pub async fn run(&mut self, app: &mut App) -> Result<(), Error> {
189        while self.handle_input(app)? {
190            self.render(app)?;
191            tokio::select! {
192                _ = app.tick_session.tick() => (),
193                _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
194            }
195        }
196        Ok(())
197    }
198
199    fn handle_input(&mut self, app: &mut App) -> Result<bool, Error> {
200        let Ok(mut events) = app.events.read_until_now() else {
201            Err(Error::Cancelled)?
202        };
203        let mut should_continue = true;
204        while let Some(event) = events.next().filter(|_| should_continue) {
205            let Event::Key(key) = event else { continue };
206            let Some(&command) = self.key_bindings.command_for(key) else {
207                continue;
208            };
209            should_continue = self.run_command(command)?;
210        }
211        Ok(should_continue)
212    }
213
214    fn render(&mut self, app: &mut App) -> Result<(), Error> {
215        app.canvas
216            .queue([screen::Command::ClearScreen(self.style().background())]);
217
218        let mut height = self.style().top_margin();
219        self.render_title(app, &mut height)?;
220        self.render_message(app, &mut height)?;
221        self.render_ok(app, &mut height)?;
222        self.render_cancel(app, &mut height)?;
223
224        app.canvas.flush()?;
225
226        Ok(())
227    }
228
229    fn render_title(
230        &mut self,
231        app: &mut App,
232        height: &mut Coord,
233    ) -> Result<(), Error> {
234        *height = text::styled(
235            app,
236            self.title(),
237            &text::Style::new_with_colors(Set(self.style().title_colors()))
238                .with_align(1, 2)
239                .with_top_margin(*height)
240                .with_left_margin(self.style().left_margin())
241                .with_right_margin(self.style().right_margin()),
242        )?;
243        *height += self.style().title_message_padding();
244        Ok(())
245    }
246
247    fn render_message(
248        &mut self,
249        app: &mut App,
250        height: &mut Coord,
251    ) -> Result<(), Error> {
252        let mut bottom_margin = self.style().bottom_margin();
253        bottom_margin += self.style().message_ok_padding();
254        bottom_margin += 1;
255        if self.is_cancellable() {
256            bottom_margin += self.style().ok_cancel_padding();
257            bottom_margin += 1;
258        }
259        bottom_margin -= 2;
260
261        *height = text::styled(
262            app,
263            self.message(),
264            &text::Style::new_with_colors(Set(self.style().title_colors()))
265                .with_align(1, 2)
266                .with_top_margin(*height)
267                .with_left_margin(self.style().left_margin())
268                .with_right_margin(self.style().right_margin())
269                .with_bottom_margin(bottom_margin),
270        )?;
271        *height += self.style().message_ok_padding();
272        Ok(())
273    }
274
275    fn render_ok(
276        &mut self,
277        app: &mut App,
278        height: &mut Coord,
279    ) -> Result<(), Error> {
280        let graphemes = self.style().ok_label().graphemes(true).count();
281        let right_padding = if graphemes % 2 == 0 { " " } else { "" };
282        let rendered = format!("{}{}", self.style().ok_label(), right_padding);
283        self.render_item(app, height, rendered, false)?;
284        *height += 1;
285        Ok(())
286    }
287
288    fn render_cancel(
289        &mut self,
290        app: &mut App,
291        height: &mut Coord,
292    ) -> Result<(), Error> {
293        if self.is_cancellable() {
294            *height += self.style().ok_cancel_padding();
295            let graphemes = self.style().cancel_label().graphemes(true).count();
296            let right_padding = if graphemes % 2 == 0 { " " } else { "" };
297            let rendered =
298                format!("{}{}", self.style().cancel_label(), right_padding);
299            self.render_item(app, height, rendered, true)?;
300        }
301        Ok(())
302    }
303
304    fn render_item(
305        &mut self,
306        app: &mut App,
307        height: &mut Coord,
308        item: String,
309        requires_cancelling: bool,
310    ) -> Result<(), Error> {
311        let is_selected = requires_cancelling == self.is_cancelling();
312        let (colors, rendered) = if is_selected {
313            let rendered = format!(
314                "{}{}{}",
315                self.style().selected_left(),
316                item,
317                self.style().selected_right(),
318            );
319            (self.style().selected_colors(), rendered)
320        } else {
321            (self.style().unselected_colors(), item)
322        };
323        text::styled(
324            app,
325            &rendered,
326            &text::Style::new_with_colors(Set(colors))
327                .with_align(1, 2)
328                .with_top_margin(*height)
329                .with_bottom_margin(app.canvas.size().y - *height - 2),
330        )?;
331        *height += 1;
332        Ok(())
333    }
334}