tui_popup/
popup.rs

1use std::fmt;
2
3use derive_setters::Setters;
4use ratatui_core::buffer::Buffer;
5use ratatui_core::layout::{Constraint, Rect};
6use ratatui_core::style::Style;
7use ratatui_core::symbols::border::Set;
8use ratatui_core::text::Line;
9use ratatui_core::widgets::{StatefulWidget, Widget};
10use ratatui_widgets::block::Block;
11use ratatui_widgets::borders::Borders;
12use ratatui_widgets::clear::Clear;
13
14use crate::{KnownSize, PopupState};
15
16/// Configuration for a popup.
17///
18/// This struct is used to configure a [`Popup`]. It can be created using
19/// [`Popup::new`](Popup::new).
20///
21/// # Example
22///
23/// ```rust
24/// use ratatui::prelude::*;
25/// use ratatui::symbols::border;
26/// use tui_popup::Popup;
27///
28/// fn render_popup(frame: &mut Frame) {
29///     let popup = Popup::new("Press any key to exit")
30///         .title("tui-popup demo")
31///         .style(Style::new().white().on_blue())
32///         .border_set(border::ROUNDED)
33///         .border_style(Style::new().bold());
34///     frame.render_widget(popup, frame.area());
35/// }
36/// ```
37#[derive(Setters)]
38#[setters(into)]
39#[non_exhaustive]
40pub struct Popup<'content, W> {
41    /// The body of the popup.
42    #[setters(skip)]
43    pub body: W,
44    /// The title of the popup.
45    pub title: Line<'content>,
46    /// The style to apply to the entire popup.
47    pub style: Style,
48    /// The borders of the popup.
49    pub borders: Borders,
50    /// The symbols used to render the border.
51    pub border_set: Set<'content>,
52    /// Border style
53    pub border_style: Style,
54}
55
56impl<W> fmt::Debug for Popup<'_, W> {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        // body does not implement Debug, so we can't use #[derive(Debug)]
59        f.debug_struct("Popup")
60            .field("body", &"...")
61            .field("title", &self.title)
62            .field("style", &self.style)
63            .field("borders", &self.borders)
64            .field("border_set", &self.border_set)
65            .field("border_style", &self.border_style)
66            .finish()
67    }
68}
69
70impl<W: PartialEq> PartialEq for Popup<'_, W> {
71    fn eq(&self, other: &Self) -> bool {
72        self.body == other.body
73            && self.title == other.title
74            && self.style == other.style
75            && self.borders == other.borders
76            && self.border_set == other.border_set
77            && self.border_style == other.border_style
78    }
79}
80
81impl<W> Popup<'_, W> {
82    /// Create a new popup with the given title and body with all the borders.
83    ///
84    /// # Parameters
85    ///
86    /// - `body` - The body of the popup. This can be any type that can be converted into a
87    ///   [`Text`](ratatui_core::text::Text).
88    ///
89    /// # Example
90    ///
91    /// ```rust
92    /// use tui_popup::Popup;
93    ///
94    /// let popup = Popup::new("Press any key to exit").title("tui-popup demo");
95    /// ```
96    pub fn new(body: W) -> Self {
97        Self {
98            body,
99            borders: Borders::ALL,
100            border_set: Set::default(),
101            border_style: Style::default(),
102            title: Line::default(),
103            style: Style::default(),
104        }
105    }
106}
107
108/// Owned render path for a popup.
109///
110/// Use this when you have an owned `Popup` value and want to render it by value. This is the
111/// simplest option for new users, and it works well with common bodies like `&str` or `String`.
112///
113/// # Example
114///
115/// ```rust
116/// # use ratatui::buffer::Buffer;
117/// # use ratatui::layout::Rect;
118/// # use ratatui::widgets::Widget;
119/// # use tui_popup::Popup;
120/// # let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
121/// let popup = Popup::new("Body");
122/// popup.render(buffer.area, &mut buffer);
123/// ```
124impl<W: KnownSize + Widget> Widget for Popup<'_, W> {
125    fn render(self, area: Rect, buf: &mut Buffer) {
126        let mut state = PopupState::default();
127        StatefulWidget::render(self, area, buf, &mut state);
128    }
129}
130
131/// Reference render path for a popup body that supports rendering by reference.
132///
133/// Use this when you want to keep a `Popup` around and render it by reference. This is helpful
134/// when the body implements `Widget` for references (such as `Text`), or when you need to avoid
135/// rebuilding the widget each frame.
136///
137/// # Example
138///
139/// ```rust
140/// # use ratatui::buffer::Buffer;
141/// # use ratatui::layout::Rect;
142/// # use ratatui::text::Text;
143/// # use ratatui::widgets::Widget;
144/// # use tui_popup::Popup;
145/// # let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
146/// let popup = Popup::new(Text::from("Body"));
147/// let popup_ref = &popup;
148/// popup_ref.render(buffer.area, &mut buffer);
149/// ```
150impl<W> Widget for &Popup<'_, W>
151where
152    W: KnownSize,
153    for<'a> &'a W: Widget,
154{
155    fn render(self, area: Rect, buf: &mut Buffer) {
156        let mut state = PopupState::default();
157        StatefulWidget::render(self, area, buf, &mut state);
158    }
159}
160
161/// Owned stateful render path for a popup.
162///
163/// Use this when you have a `PopupState` that you want to update across frames and you can pass
164/// the popup by value in the render call.
165///
166/// # Example
167///
168/// ```rust
169/// # use ratatui::buffer::Buffer;
170/// # use ratatui::layout::Rect;
171/// # use ratatui::widgets::StatefulWidget;
172/// # use tui_popup::{Popup, PopupState};
173/// # let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
174/// let popup = Popup::new("Body");
175/// let mut state = PopupState::default();
176/// popup.render(buffer.area, &mut buffer, &mut state);
177/// ```
178impl<W: KnownSize + Widget> StatefulWidget for Popup<'_, W> {
179    type State = PopupState;
180
181    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
182        let area = area.clamp(buf.area);
183        let popup_area = self.popup_area(state, area);
184
185        state.area.replace(popup_area);
186
187        Clear.render(popup_area, buf);
188        let block = Block::default()
189            .borders(self.borders)
190            .border_set(self.border_set)
191            .border_style(self.border_style)
192            .title(self.title)
193            .style(self.style);
194        let inner_area = block.inner(popup_area);
195        block.render(popup_area, buf);
196
197        self.body.render(inner_area, buf);
198    }
199}
200
201/// Reference stateful render path for a popup body that renders by reference.
202///
203/// Use this when you have long-lived popup data and want to update `PopupState` without moving the
204/// popup each frame. This requires the body to support rendering by reference.
205///
206/// # Example
207///
208/// ```rust
209/// # use ratatui::buffer::Buffer;
210/// # use ratatui::layout::Rect;
211/// # use ratatui::text::Text;
212/// # use ratatui::widgets::StatefulWidget;
213/// # use tui_popup::{Popup, PopupState};
214/// # let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
215/// let popup = Popup::new(Text::from("Body"));
216/// let popup_ref = &popup;
217/// let mut state = PopupState::default();
218/// popup_ref.render(buffer.area, &mut buffer, &mut state);
219/// ```
220impl<W> StatefulWidget for &Popup<'_, W>
221where
222    W: KnownSize,
223    for<'a> &'a W: Widget,
224{
225    type State = PopupState;
226
227    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
228        let area = area.clamp(buf.area);
229        let popup_area = self.popup_area(state, area);
230
231        state.area.replace(popup_area);
232
233        Clear.render(popup_area, buf);
234        let block = Block::default()
235            .borders(self.borders)
236            .border_set(self.border_set)
237            .border_style(self.border_style)
238            .title(self.title.clone())
239            .style(self.style);
240        let inner_area = block.inner(popup_area);
241        block.render(popup_area, buf);
242
243        self.body.render(inner_area, buf);
244    }
245}
246
247impl<W: KnownSize> Popup<'_, W> {
248    fn popup_area(&self, state: &mut PopupState, area: Rect) -> Rect {
249        if let Some(current) = state.area.take() {
250            return current.clamp(area);
251        }
252
253        let has_top = self.borders.intersects(Borders::TOP);
254        let has_bottom = self.borders.intersects(Borders::BOTTOM);
255        let has_left = self.borders.intersects(Borders::LEFT);
256        let has_right = self.borders.intersects(Borders::RIGHT);
257
258        let border_height = usize::from(has_top) + usize::from(has_bottom);
259        let border_width = usize::from(has_left) + usize::from(has_right);
260
261        let height = self.body.height().saturating_add(border_height);
262        let width = self.body.width().saturating_add(border_width);
263
264        let height = u16::try_from(height).unwrap_or(area.height);
265        let width = u16::try_from(width).unwrap_or(area.width);
266
267        area.centered(Constraint::Length(width), Constraint::Length(height))
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use pretty_assertions::assert_eq;
274    use ratatui_core::text::Text;
275
276    use super::*;
277
278    struct RefBody;
279
280    impl KnownSize for RefBody {
281        fn width(&self) -> usize {
282            11
283        }
284
285        fn height(&self) -> usize {
286            1
287        }
288    }
289
290    impl Widget for &RefBody {
291        fn render(self, area: Rect, buf: &mut Buffer) {
292            "Hello World".render(area, buf);
293        }
294    }
295
296    #[test]
297    fn new() {
298        let popup = Popup::new("Test Body");
299        assert_eq!(
300            popup,
301            Popup {
302                body: "Test Body", // &str is a widget
303                borders: Borders::ALL,
304                border_set: Set::default(),
305                border_style: Style::default(),
306                title: Line::default(),
307                style: Style::default(),
308            }
309        );
310    }
311
312    #[test]
313    fn render() {
314        let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
315        let mut state = PopupState::default();
316        let expected = Buffer::with_lines([
317            "                    ",
318            "    ┌Title──────┐   ",
319            "    │Hello World│   ",
320            "    └───────────┘   ",
321            "                    ",
322        ]);
323
324        // Check that a popup ref can render a body widget that supports rendering by reference.
325        let popup = Popup::new(RefBody).title("Title");
326        StatefulWidget::render(&popup, buffer.area, &mut buffer, &mut state);
327        assert_eq!(buffer, expected);
328
329        // Check that an owned popup can render a widget defined by a ref value (e.g. `&str`).
330        let popup = Popup::new("Hello World").title("Title");
331        StatefulWidget::render(popup, buffer.area, &mut buffer, &mut state);
332        assert_eq!(buffer, expected);
333
334        // Check that an owned popup can render a widget defined by a owned value (e.g. `String`).
335        let popup = Popup::new("Hello World".to_string()).title("Title");
336        StatefulWidget::render(popup, buffer.area, &mut buffer, &mut state);
337        assert_eq!(buffer, expected);
338
339        // Check that a popup ref can render a reference-supported body with default state.
340        let popup = Popup::new(Text::from("Hello World")).title("Title");
341        Widget::render(&popup, buffer.area, &mut buffer);
342        assert_eq!(buffer, expected);
343
344        // Check that an owned popup can render a ref value (e.g. `&str`), with default state.
345        let popup = Popup::new("Hello World").title("Title");
346        Widget::render(popup, buffer.area, &mut buffer);
347        assert_eq!(buffer, expected);
348
349        // Check that an owned popup can render an owned value (e.g. `String`), with default state.
350        let popup = Popup::new("Hello World".to_string()).title("Title");
351        Widget::render(popup, buffer.area, &mut buffer);
352        assert_eq!(buffer, expected);
353    }
354}