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#[derive(Setters)]
38#[setters(into)]
39#[non_exhaustive]
40pub struct Popup<'content, W> {
41 #[setters(skip)]
43 pub body: W,
44 pub title: Line<'content>,
46 pub style: Style,
48 pub borders: Borders,
50 pub border_set: Set<'content>,
52 pub border_style: Style,
54}
55
56impl<W> fmt::Debug for Popup<'_, W> {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 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 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
108impl<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
131impl<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
161impl<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
201impl<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", 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 let popup = Popup::new(RefBody).title("Title");
326 StatefulWidget::render(&popup, buffer.area, &mut buffer, &mut state);
327 assert_eq!(buffer, expected);
328
329 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 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 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 let popup = Popup::new("Hello World").title("Title");
346 Widget::render(popup, buffer.area, &mut buffer);
347 assert_eq!(buffer, expected);
348
349 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}