ratatui_toaster/engine.rs
1//! A toast engine for displaying temporary messages in a terminal UI.
2//! The `ToastEngine` manages the display of toasts, which are temporary messages that appear on the screen for a short duration. It supports different types of toasts (info, success, warning, error) and allows customization of their position and duration.
3//!
4//! The `ToastEngine` can be integrated into a terminal UI application using the `ratatui` crate. It provides a builder pattern for creating toasts and handles the timing for automatically hiding toasts after a specified duration.
5//! # Tokio Integration
6//! The `tokio` feature can be used to tightly integrate the toast engine with applications that use an event based pattern. In your
7//! `Action` enum (or equivalent), add a variant that can be converted from `ToastMessage`. For example:
8//! ```rust
9//! enum Action {
10//! ShowToast(ToastMessage),
11//! // other variants...
12//! }
13//! ```
14//! Then, when you want to show a toast, you can send a `ToastMessage::Show` action through your application's event system, although you do need
15//! to handle the `Show` event yourself. When the toast times out, the `ToastEngine` will automatically send a `ToastMessage::Hide` action, which you should also handle to hide the toast.
16//! Disable the `tokio` feature if you want to manage the timing of hiding toasts yourself, or if your application does not use an event based pattern.
17//!
18//! # Animating Toasts
19//! The current implementation does not include animations for showing or hiding toasts. However, you can
20//! use libraries like [tachyonfx](https://github.com/ratatui/tachyonfx) to add animations to your toasts. You would need to implement the animation logic in your event handling code, triggering animations when showing or hiding toasts based on the `ToastMessage` actions.
21use std::borrow::Cow;
22#[cfg(not(feature = "tokio"))]
23use std::marker::PhantomData;
24
25use ratatui::{
26 layout::{Constraint, Rect, Size},
27 widgets::{Clear, Widget, WidgetRef},
28};
29use textwrap::wrap;
30
31use crate::widget::Toast;
32
33const DEFAULT_MAX_TOAST_WIDTH: u16 = 50;
34
35/// A toast engine for displaying temporary messages in a terminal UI.
36/// The `ToastEngine` manages the display of toasts, which are temporary messages that appear on the screen for a short duration. It supports different types of toasts (info, success, warning, error) and allows customization of their position and duration.
37/// You can call `show_toast` to display a toast, and `hide_toast` to hide the toast. To animate,
38/// you can get the area of the toast using `toast_area` and implement your animation logic based on that area. #[derive(Debug)]
39/// Caveat: If you're not using the `tokio` feature, create a `ToastEngine<()>`. There is a (hacky) impl to make it work without the `tokio` feature.
40pub struct ToastEngine<A>
41where
42 A: From<ToastMessage> + Send + 'static,
43{
44 area: Rect,
45 default_duration: std::time::Duration,
46 #[cfg(feature = "tokio")]
47 tx: Option<tokio::sync::mpsc::Sender<A>>,
48 #[cfg(not(feature = "tokio"))]
49 tx: Option<PhantomData<A>>,
50 toast_area: Rect,
51 current_toast: Option<Toast>,
52}
53
54/// A builder for creating a `ToastEngine`. It allows you to set the default duration for toasts, and an optional channel sender for sending toast messages (if using the `tokio` feature).
55pub struct ToastEngineBuilder<A>
56where
57 A: From<ToastMessage> + Send + 'static,
58{
59 area: Rect,
60 default_duration: std::time::Duration,
61 #[cfg(feature = "tokio")]
62 tx: Option<tokio::sync::mpsc::Sender<A>>,
63 #[cfg(not(feature = "tokio"))]
64 tx: Option<PhantomData<A>>,
65}
66
67impl<A> ToastEngineBuilder<A>
68where
69 A: From<ToastMessage> + Send + 'static,
70{
71 /// Creates a new `ToastEngineBuilder` with the specified area for displaying toasts. The default duration for toasts is set to 3 seconds, and no channel sender is configured by default.
72 pub fn new(area: Rect) -> Self {
73 Self {
74 area,
75 default_duration: std::time::Duration::from_secs(3),
76 tx: None,
77 }
78 }
79
80 /// Sets the default duration for toasts. This duration will be used when showing a toast if no specific duration is provided.
81 pub fn default_duration(mut self, duration: std::time::Duration) -> Self {
82 self.default_duration = duration;
83 self
84 }
85
86 /// Configures a channel sender for sending toast messages. This is used when the `tokio` feature is enabled to allow the `ToastEngine` to send messages to hide toasts after the duration expires.
87 #[cfg(feature = "tokio")]
88 pub fn action_tx(mut self, tx: tokio::sync::mpsc::Sender<A>) -> Self {
89 self.tx = Some(tx);
90 self
91 }
92
93 /// Builds the `ToastEngine` using the configured settings. This method consumes the builder and returns a new instance of `ToastEngine`.
94 pub fn build(self) -> ToastEngine<A> {
95 ToastEngine::from_builder(self)
96 }
97}
98
99/// The type of toast to display. This enum defines the different types of toasts that can be shown, such as informational messages, success messages, warnings, and errors. Each variant can be styled differently when rendered.
100#[derive(Debug, Default, Clone, Copy)]
101pub enum ToastType {
102 #[default]
103 Info,
104 Success,
105 Warning,
106 Error,
107}
108
109/// The position on the screen where the toast should be displayed. This enum defines various positions for toasts, including top-left, top-right, bottom-left, bottom-right, and center. The `ToastEngine` uses this information to calculate the appropriate area for rendering the toast based on the specified position.
110#[derive(Debug, Default, Clone, Copy)]
111pub enum ToastPosition {
112 #[default]
113 TopLeft,
114 TopRight,
115 BottomLeft,
116 BottomRight,
117 Center,
118}
119
120/// The constraint for the toast's size. This enum defines how the size of the toast should be determined. The `Auto` variant allows the toast to automatically size itself based on the message content, while the `Uniform` and `Manual` variants allow for more specific control over the width and height of the toast.
121#[derive(Debug, Default)]
122pub enum ToastConstraint {
123 #[default]
124 Auto,
125 Uniform(Constraint),
126 Manual {
127 width: Constraint,
128 height: Constraint,
129 },
130}
131
132/// The messages that can be sent to the `ToastEngine` to control the display of toasts. The `Show` variant contains the message to display, the type of toast, and its position, while the `Hide` variant indicates that any currently displayed toast should be hidden.
133///
134///NOTE: You do have to handle the events yourself. Usually, its as simple as matching on the `ToastMessage` in your event loop and calling the appropriate methods on the `ToastEngine` to show or hide toasts based on the received messages.
135#[derive(Debug, Clone)]
136pub enum ToastMessage {
137 Show {
138 message: String,
139 toast_type: ToastType,
140 position: ToastPosition,
141 },
142 Hide,
143}
144
145/// A builder for creating a toast message. This struct allows you to specify the message content, type, position, and size constraints for a toast before showing it using the `ToastEngine`. The builder pattern provides a convenient way to configure the properties of a toast in a fluent manner.
146#[derive(Debug, Default)]
147pub struct ToastBuilder {
148 message: Cow<'static, str>,
149 toast_type: ToastType,
150 position: ToastPosition,
151 constraint: ToastConstraint,
152}
153
154impl<A> ToastEngine<A>
155where
156 A: From<ToastMessage> + Send + 'static,
157{
158 /// Creates a new `ToastEngine`. Consider using the `ToastEngineBuilder` instead.
159 pub fn new(
160 ToastEngine {
161 area,
162 default_duration,
163 tx,
164 ..
165 }: Self,
166 ) -> Self {
167 Self {
168 area,
169 default_duration,
170 tx,
171 current_toast: None,
172 toast_area: Rect::default(),
173 }
174 }
175
176 /// Creates a new `ToastEngine` from a `ToastEngineBuilder`. This method takes the configuration from the builder and initializes the `ToastEngine` accordingly. It sets up the area for displaying toasts, the default duration for toasts, and any channel sender if provided (when using the `tokio` feature).
177 pub fn from_builder(
178 ToastEngineBuilder {
179 area,
180 default_duration,
181 tx,
182 ..
183 }: ToastEngineBuilder<A>,
184 ) -> Self {
185 Self {
186 area,
187 default_duration,
188 tx,
189 current_toast: None,
190 toast_area: Rect::default(),
191 }
192 }
193
194 /// Shows a toast message using the provided `ToastBuilder`. This method calculates the area for the toast based on the message content and the specified position, creates a new `Toast` instance, and sets it as the current toast to be rendered. If the `tokio` feature is enabled and a channel sender is configured, it also spawns a task to automatically hide the toast after the default duration.
195 pub fn show_toast(&mut self, toast: ToastBuilder) {
196 let toast_area = calculate_toast_area(&toast, self.area);
197 self.toast_area = toast_area;
198 let toast = Toast::new(&toast.message, toast.toast_type);
199 self.current_toast = Some(toast);
200 #[cfg(feature = "tokio")]
201 if let Some(tx) = &self.tx {
202 let tx_clone = tx.clone();
203 let duration = self.default_duration;
204 tokio::spawn(async move {
205 tokio::time::sleep(duration).await;
206 let _ = tx_clone.send(ToastMessage::Hide.into()).await;
207 });
208 }
209 }
210
211 /// Get the area where the toast will be rendered.
212 pub fn toast_area(&self) -> Rect {
213 self.toast_area
214 }
215
216 /// Whether a toast is currently being displayed.
217 pub fn has_toast(&self) -> bool {
218 self.current_toast.is_some()
219 }
220
221 /// Hides the currently displayed toast, if any. This method sets the current toast to `None`, which will cause it to no longer be rendered on the screen.
222 pub fn hide_toast(&mut self) {
223 self.current_toast = None;
224 }
225
226 /// Sets the area for the toast engine. This method allows you to update the area where toasts will be displayed, which can be useful if the layout of your terminal UI changes and you need to adjust the toast display area accordingly.
227 pub fn set_area(&mut self, area: Rect) {
228 self.area = area;
229 }
230}
231
232impl ToastBuilder {
233 /// Create a new instance of a `ToastBuilder`
234 pub fn new(message: Cow<'static, str>) -> Self {
235 Self {
236 message,
237 toast_type: ToastType::Info,
238 position: ToastPosition::TopRight,
239 constraint: ToastConstraint::Auto,
240 }
241 }
242
243 pub fn toast_type(mut self, toast_type: ToastType) -> Self {
244 self.toast_type = toast_type;
245 self
246 }
247
248 pub fn position(mut self, position: ToastPosition) -> Self {
249 self.position = position;
250 self
251 }
252
253 pub fn constraint(mut self, constraint: ToastConstraint) -> Self {
254 self.constraint = constraint;
255 self
256 }
257}
258
259fn calculate_toast_area(
260 ToastBuilder {
261 message,
262 position,
263 constraint,
264 ..
265 }: &ToastBuilder,
266 area: Rect,
267) -> Rect {
268 use ToastConstraint::*;
269 use ToastPosition::*;
270 const PADDING: u16 = 2;
271
272 let width = match constraint {
273 Auto => std::cmp::min(DEFAULT_MAX_TOAST_WIDTH, message.len() as u16 + PADDING * 2),
274 Uniform(c) => area.centered_horizontally(*c).width,
275 Manual { width, .. } => area.centered_horizontally(*width).width,
276 };
277 let wrapped_text = wrap(message, width as usize);
278 let height = match constraint {
279 Auto => wrapped_text.len() as u16 + PADDING,
280 Uniform(c) => area.centered_vertically(*c).height + PADDING,
281 Manual { height, .. } => area.centered_vertically(*height).height + PADDING,
282 };
283 if let Center = position {
284 return area.centered(width.into(), height.into());
285 }
286 position.calculate_position(area, Size { width, height })
287}
288
289impl ToastPosition {
290 fn calculate_position(&self, area: Rect, Size { width, height }: Size) -> Rect {
291 use ToastPosition::*;
292 match self {
293 TopLeft => Rect {
294 x: area.x,
295 y: area.y,
296 width,
297 height,
298 },
299 TopRight => Rect {
300 x: area.x + area.width.saturating_sub(width),
301 y: area.y,
302 width,
303 height,
304 },
305 BottomLeft => Rect {
306 x: area.x,
307 y: area.y + area.height.saturating_sub(height),
308 width,
309 height,
310 },
311 BottomRight => Rect {
312 x: area.x + area.width.saturating_sub(width),
313 y: area.y + area.height.saturating_sub(height),
314 width,
315 height,
316 },
317 Center => Rect {
318 x: area.x + (area.width.saturating_sub(width)) / 2,
319 y: area.y + (area.height.saturating_sub(height)) / 2,
320 width,
321 height,
322 },
323 }
324 }
325}
326
327impl From<ToastType> for ratatui::style::Color {
328 fn from(value: ToastType) -> Self {
329 use ToastType::*;
330 match value {
331 Info => Self::Blue,
332 Success => Self::Green,
333 Warning => Self::Yellow,
334 Error => Self::Red,
335 }
336 }
337}
338
339impl<A> WidgetRef for ToastEngine<A>
340where
341 A: From<ToastMessage> + Send + 'static,
342{
343 fn render_ref(&self, _area: Rect, buf: &mut ratatui::buffer::Buffer) {
344 if self.current_toast.is_some() {
345 Clear.render(self.toast_area, buf);
346 }
347 self.current_toast.render_ref(self.toast_area, buf);
348 }
349}
350
351impl<A> Widget for &ToastEngine<A>
352where
353 A: From<ToastMessage> + Send + 'static,
354{
355 fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
356 self.render_ref(area, buf);
357 }
358}
359
360impl From<ToastMessage> for () {
361 fn from(_value: ToastMessage) -> Self {}
362}