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}