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#[derive(Debug, Setters)]
33#[setters(into)]
34#[non_exhaustive]
35pub struct Popup<'content, W: SizedWidgetRef> {
36 #[setters(skip)]
38 pub body: W,
39 pub title: Line<'content>,
41 pub style: Style,
43 pub borders: Borders,
45 pub border_set: Set,
47 pub border_style: Style,
49}
50
51pub trait SizedWidgetRef: WidgetRef + Debug {
58 fn width(&self) -> usize;
59 fn height(&self) -> usize;
60}
61
62impl<'content, W: SizedWidgetRef> Popup<'content, W> {
63 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 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
186fn 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}