tui_popup/
popup.rs

1use std::fmt::Debug;
2
3use crate::PopupState;
4use derive_setters::Setters;
5use ratatui::{
6    prelude::{Buffer, Line, Rect, Style, Text},
7    symbols::border::Set,
8    widgets::{Block, Borders, Clear, StatefulWidgetRef, Widget, WidgetRef},
9};
10use std::cmp::min;
11
12/// Configuration for a popup.
13///
14/// This struct is used to configure a [`Popup`]. It can be created using
15/// [`Popup::new`](Popup::new).
16///
17/// # Example
18///
19/// ```rust
20/// use ratatui::{prelude::*, symbols::border};
21/// use tui_popup::Popup;
22///
23/// fn render_popup(frame: &mut Frame) {
24///     let popup = Popup::new("Press any key to exit")
25///         .title("tui-popup demo")
26///         .style(Style::new().white().on_blue())
27///         .border_set(border::ROUNDED)
28///         .border_style(Style::new().bold());
29///     frame.render_widget(&popup, frame.size());
30/// }
31/// ```
32#[derive(Debug, Setters)]
33#[setters(into)]
34#[non_exhaustive]
35pub struct Popup<'content, W: SizedWidgetRef> {
36    /// The body of the popup.
37    #[setters(skip)]
38    pub body: W,
39    /// The title of the popup.
40    pub title: Line<'content>,
41    /// The style to apply to the entire popup.
42    pub style: Style,
43    /// The borders of the popup.
44    pub borders: Borders,
45    /// The symbols used to render the border.
46    pub border_set: Set,
47    /// Border style
48    pub border_style: Style,
49}
50
51/// A trait for widgets that have a fixed size.
52///
53/// This trait allows the popup to automatically size itself based on the size of the body widget.
54/// Implementing this trait for a widget allows it to be used as the body of a popup. You can also
55/// wrap existing widgets in a newtype and implement this trait for the newtype to use them as the
56/// body of a popup.
57pub trait SizedWidgetRef: WidgetRef + Debug {
58    fn width(&self) -> usize;
59    fn height(&self) -> usize;
60}
61
62impl<'content, W: SizedWidgetRef> Popup<'content, W> {
63    /// Create a new popup with the given title and body with all the borders.
64    ///
65    /// # Parameters
66    ///
67    /// - `body` - The body of the popup. This can be any type that can be converted into a
68    ///   [`Text`].
69    ///
70    /// # Example
71    ///
72    /// ```rust
73    /// use tui_popup::Popup;
74    ///
75    /// let popup = Popup::new("Press any key to exit").title("tui-popup demo");
76    /// ```
77    pub fn new(body: W) -> Self {
78        Self {
79            body,
80            borders: Borders::ALL,
81            border_set: Set::default(),
82            border_style: Style::default(),
83            title: Line::default(),
84            style: Style::default(),
85        }
86    }
87}
88
89impl SizedWidgetRef for Text<'_> {
90    fn width(&self) -> usize {
91        self.width()
92    }
93
94    fn height(&self) -> usize {
95        self.height()
96    }
97}
98
99impl SizedWidgetRef for &str {
100    fn width(&self) -> usize {
101        Text::from(*self).width()
102    }
103
104    fn height(&self) -> usize {
105        Text::from(*self).height()
106    }
107}
108
109#[derive(Debug)]
110pub struct SizedWrapper<W: Debug> {
111    pub inner: W,
112    pub width: usize,
113    pub height: usize,
114}
115
116impl<W: WidgetRef + Debug> WidgetRef for SizedWrapper<W> {
117    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
118        self.inner.render_ref(area, buf);
119    }
120}
121
122impl<W: WidgetRef + Debug> SizedWidgetRef for SizedWrapper<W> {
123    fn width(&self) -> usize {
124        self.width
125    }
126
127    fn height(&self) -> usize {
128        self.height
129    }
130}
131
132impl<W: SizedWidgetRef> WidgetRef for Popup<'_, W> {
133    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
134        let mut state = PopupState::default();
135        StatefulWidgetRef::render_ref(self, area, buf, &mut state);
136    }
137}
138
139impl<W: SizedWidgetRef> StatefulWidgetRef for Popup<'_, W> {
140    type State = PopupState;
141
142    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
143        let area = if let Some(next) = state.area.take() {
144            // ensure that the popup remains on screen
145            let width = min(next.width, area.width);
146            let height = min(next.height, area.height);
147            let x = next.x.clamp(buf.area.x, area.right() - width);
148            let y = next.y.clamp(buf.area.y, area.bottom() - height);
149
150            Rect::new(x, y, width, height)
151        } else {
152            let border_height = usize::from(self.borders.intersects(Borders::TOP))
153                + usize::from(self.borders.intersects(Borders::BOTTOM));
154            let border_width = usize::from(self.borders.intersects(Borders::LEFT))
155                + usize::from(self.borders.intersects(Borders::RIGHT));
156
157            let height = self
158                .body
159                .height()
160                .saturating_add(border_height)
161                .try_into()
162                .unwrap_or(area.height);
163            let width = self
164                .body
165                .width()
166                .saturating_add(border_width)
167                .try_into()
168                .unwrap_or(area.width);
169            centered_rect(width, height, area)
170        };
171
172        state.area.replace(area);
173
174        Clear.render(area, buf);
175        let block = Block::default()
176            .borders(self.borders)
177            .border_set(self.border_set)
178            .border_style(self.border_style)
179            .title(self.title.clone())
180            .style(self.style);
181        block.render_ref(area, buf);
182        self.body.render_ref(block.inner(area), buf);
183    }
184}
185
186/// Create a rectangle centered in the given area.
187fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
188    Rect {
189        x: area.width.saturating_sub(width) / 2,
190        y: area.height.saturating_sub(height) / 2,
191        width: min(width, area.width),
192        height: min(height, area.height),
193    }
194}