Skip to main content

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}