1#![forbid(unsafe_code)]
2
3use crate::{StatefulWidget, Widget};
11use ftui_core::geometry::{Rect, Sides};
12use ftui_render::frame::Frame;
13
14#[derive(Debug, Clone)]
16pub struct Padding<W> {
17 inner: W,
18 padding: Sides,
19}
20
21impl<W> Padding<W> {
22 pub const fn new(inner: W, padding: Sides) -> Self {
24 Self { inner, padding }
25 }
26
27 pub const fn padding(mut self, padding: Sides) -> Self {
29 self.padding = padding;
30 self
31 }
32
33 pub const fn padding_sides(&self) -> Sides {
35 self.padding
36 }
37
38 #[inline]
40 pub fn inner_area(&self, area: Rect) -> Rect {
41 area.inner(self.padding)
42 }
43
44 pub const fn inner(&self) -> &W {
46 &self.inner
47 }
48
49 pub fn inner_mut(&mut self) -> &mut W {
51 &mut self.inner
52 }
53
54 pub fn into_inner(self) -> W {
56 self.inner
57 }
58}
59
60struct ScissorGuard<'a, 'pool> {
61 frame: &'a mut Frame<'pool>,
62}
63
64impl<'a, 'pool> ScissorGuard<'a, 'pool> {
65 fn new(frame: &'a mut Frame<'pool>, rect: Rect) -> Self {
66 frame.buffer.push_scissor(rect);
67 Self { frame }
68 }
69}
70
71impl Drop for ScissorGuard<'_, '_> {
72 fn drop(&mut self) {
73 self.frame.buffer.pop_scissor();
74 }
75}
76
77impl<W: Widget> Widget for Padding<W> {
78 fn render(&self, area: Rect, frame: &mut Frame) {
79 #[cfg(feature = "tracing")]
80 let _span = tracing::debug_span!(
81 "widget_render",
82 widget = "Padding",
83 x = area.x,
84 y = area.y,
85 w = area.width,
86 h = area.height
87 )
88 .entered();
89
90 if area.is_empty() {
91 return;
92 }
93
94 let inner = self.inner_area(area);
95 if inner.is_empty() {
96 return;
97 }
98
99 let guard = ScissorGuard::new(frame, inner);
100 self.inner.render(inner, guard.frame);
101 }
102
103 fn is_essential(&self) -> bool {
104 self.inner.is_essential()
105 }
106}
107
108impl<W: StatefulWidget> StatefulWidget for Padding<W> {
109 type State = W::State;
110
111 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
112 #[cfg(feature = "tracing")]
113 let _span = tracing::debug_span!(
114 "widget_render",
115 widget = "PaddingStateful",
116 x = area.x,
117 y = area.y,
118 w = area.width,
119 h = area.height
120 )
121 .entered();
122
123 if area.is_empty() {
124 return;
125 }
126
127 let inner = self.inner_area(area);
128 if inner.is_empty() {
129 return;
130 }
131
132 let guard = ScissorGuard::new(frame, inner);
133 self.inner.render(inner, guard.frame, state);
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use ftui_render::buffer::Buffer;
141 use ftui_render::cell::Cell;
142 use ftui_render::grapheme_pool::GraphemePool;
143
144 fn buf_to_lines(buf: &Buffer) -> Vec<String> {
145 let mut lines = Vec::new();
146 for y in 0..buf.height() {
147 let mut row = String::with_capacity(buf.width() as usize);
148 for x in 0..buf.width() {
149 let ch = buf
150 .get(x, y)
151 .and_then(|c| c.content.as_char())
152 .unwrap_or(' ');
153 row.push(ch);
154 }
155 lines.push(row);
156 }
157 lines
158 }
159
160 #[derive(Debug, Clone, Copy)]
161 struct Fill(char);
162
163 impl Widget for Fill {
164 fn render(&self, area: Rect, frame: &mut Frame) {
165 for y in area.y..area.bottom() {
166 for x in area.x..area.right() {
167 frame.buffer.set(x, y, Cell::from_char(self.0));
168 }
169 }
170 }
171 }
172
173 #[derive(Debug, Clone, Copy)]
174 struct Naughty;
175
176 impl Widget for Naughty {
177 fn render(&self, _area: Rect, frame: &mut Frame) {
178 frame.buffer.set(0, 0, Cell::from_char('X'));
180 frame.buffer.set(2, 2, Cell::from_char('Y'));
181 }
182 }
183
184 #[derive(Debug, Clone, Copy)]
185 struct Boom;
186
187 impl Widget for Boom {
188 fn render(&self, _area: Rect, _frame: &mut Frame) {
189 unreachable!("boom");
190 }
191 }
192
193 #[test]
194 fn inner_area_zero_padding_is_identity() {
195 let pad = Padding::new(Fill('A'), Sides::all(0));
196 let area = Rect::new(3, 4, 10, 7);
197 assert_eq!(pad.inner_area(area), area);
198 }
199
200 #[test]
201 fn inner_area_asymmetric_padding() {
202 let pad = Padding::new(Fill('A'), Sides::new(1, 2, 1, 3));
203 let area = Rect::new(0, 0, 10, 4);
204 assert_eq!(pad.inner_area(area), Rect::new(3, 1, 5, 2));
205 }
206
207 #[test]
208 fn inner_area_clamps_when_padding_exceeds_area() {
209 let pad = Padding::new(Fill('A'), Sides::all(5));
210 let inner = pad.inner_area(Rect::new(0, 0, 2, 2));
211 assert_eq!(inner.width, 0);
212 assert_eq!(inner.height, 0);
213 }
214
215 #[test]
216 fn render_padding_shifts_child_and_leaves_gutter_blank() {
217 let pad = Padding::new(Fill('A'), Sides::all(1));
218 let area = Rect::from_size(5, 5);
219 let mut pool = GraphemePool::new();
220 let mut frame = Frame::new(5, 5, &mut pool);
221 pad.render(area, &mut frame);
222
223 assert_eq!(
224 buf_to_lines(&frame.buffer),
225 vec![
226 " ".to_string(),
227 " AAA ".to_string(),
228 " AAA ".to_string(),
229 " AAA ".to_string(),
230 " ".to_string(),
231 ]
232 );
233 }
234
235 #[test]
236 fn render_is_clipped_to_inner_rect_via_scissor() {
237 let pad = Padding::new(Naughty, Sides::all(1));
238 let area = Rect::from_size(5, 5);
239 let mut pool = GraphemePool::new();
240 let mut frame = Frame::new(5, 5, &mut pool);
241 pad.render(area, &mut frame);
242
243 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
245 assert_eq!(frame.buffer.get(2, 2).unwrap().content.as_char(), Some('Y'));
247 }
248
249 #[test]
250 fn scissor_stack_restores_on_panic() {
251 let pad = Padding::new(Boom, Sides::all(1));
252 let area = Rect::from_size(5, 5);
253 let mut pool = GraphemePool::new();
254 let mut frame = Frame::new(5, 5, &mut pool);
255 assert_eq!(frame.buffer.scissor_depth(), 1);
256
257 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
258 pad.render(area, &mut frame);
259 }));
260 assert!(result.is_err());
261 assert_eq!(frame.buffer.scissor_depth(), 1);
262 }
263
264 #[test]
265 fn render_empty_area_is_noop() {
266 let pad = Padding::new(Fill('X'), Sides::all(1));
267 let area = Rect::new(0, 0, 0, 0);
268 let mut pool = GraphemePool::new();
269 let mut frame = Frame::new(5, 5, &mut pool);
270 pad.render(area, &mut frame);
271 for y in 0..5 {
272 for x in 0..5u16 {
273 assert!(frame.buffer.get(x, y).unwrap().is_empty());
274 }
275 }
276 }
277
278 #[test]
279 fn padding_larger_than_area_renders_nothing() {
280 let pad = Padding::new(Fill('X'), Sides::all(10));
281 let area = Rect::from_size(5, 5);
282 let mut pool = GraphemePool::new();
283 let mut frame = Frame::new(5, 5, &mut pool);
284 pad.render(area, &mut frame);
285 for y in 0..5 {
287 for x in 0..5u16 {
288 assert!(frame.buffer.get(x, y).unwrap().is_empty());
289 }
290 }
291 }
292
293 #[test]
294 fn asymmetric_padding_top_left() {
295 let pad = Padding::new(Fill('A'), Sides::new(2, 0, 0, 1));
296 let area = Rect::from_size(5, 5);
297 let mut pool = GraphemePool::new();
298 let mut frame = Frame::new(5, 5, &mut pool);
299 pad.render(area, &mut frame);
300
301 let lines = buf_to_lines(&frame.buffer);
302 assert_eq!(lines[0], " "); assert_eq!(lines[1], " "); assert_eq!(lines[2], " AAAA"); assert_eq!(lines[3], " AAAA");
307 assert_eq!(lines[4], " AAAA");
308 }
309
310 #[test]
311 fn padding_sides_accessor() {
312 let pad = Padding::new(Fill('A'), Sides::new(1, 2, 3, 4));
313 let s = pad.padding_sides();
314 assert_eq!(s.top, 1);
315 assert_eq!(s.right, 2);
316 assert_eq!(s.bottom, 3);
317 assert_eq!(s.left, 4);
318 }
319
320 #[test]
321 fn inner_accessor() {
322 let pad = Padding::new(Fill('A'), Sides::all(0));
323 assert_eq!(pad.inner().0, 'A');
324 }
325
326 #[test]
327 fn inner_mut_accessor() {
328 let mut pad = Padding::new(Fill('A'), Sides::all(0));
329 pad.inner_mut().0 = 'B';
330 assert_eq!(pad.inner().0, 'B');
331 }
332
333 #[test]
334 fn into_inner() {
335 let pad = Padding::new(Fill('Z'), Sides::all(0));
336 let inner = pad.into_inner();
337 assert_eq!(inner.0, 'Z');
338 }
339
340 #[test]
341 fn padding_builder() {
342 let pad = Padding::new(Fill('A'), Sides::all(0)).padding(Sides::all(2));
343 assert_eq!(pad.padding_sides(), Sides::all(2));
344 }
345
346 #[test]
347 fn is_essential_delegates_to_inner() {
348 #[derive(Debug, Clone, Copy)]
349 struct Essential;
350 impl Widget for Essential {
351 fn render(&self, _: Rect, _: &mut Frame) {}
352 fn is_essential(&self) -> bool {
353 true
354 }
355 }
356
357 let non_essential = Padding::new(Fill('A'), Sides::all(0));
358 assert!(!non_essential.is_essential());
359
360 let essential = Padding::new(Essential, Sides::all(0));
361 assert!(essential.is_essential());
362 }
363
364 #[test]
365 fn stateful_render_with_padding() {
366 #[derive(Debug, Clone, Copy)]
367 struct StateFill(char);
368
369 impl StatefulWidget for StateFill {
370 type State = usize;
371 fn render(&self, area: Rect, frame: &mut Frame, state: &mut usize) {
372 *state += 1;
373 for y in area.y..area.bottom() {
374 for x in area.x..area.right() {
375 frame.buffer.set(x, y, Cell::from_char(self.0));
376 }
377 }
378 }
379 }
380
381 let pad = Padding::new(StateFill('S'), Sides::all(1));
382 let area = Rect::from_size(5, 5);
383 let mut pool = GraphemePool::new();
384 let mut frame = Frame::new(5, 5, &mut pool);
385 let mut state: usize = 0;
386 StatefulWidget::render(&pad, area, &mut frame, &mut state);
387
388 assert_eq!(state, 1);
389 let lines = buf_to_lines(&frame.buffer);
390 assert_eq!(lines[0], " ");
391 assert_eq!(lines[1], " SSS ");
392 assert_eq!(lines[2], " SSS ");
393 }
394
395 #[test]
396 fn large_padding_single_cell_inner() {
397 let pad = Padding::new(Fill('X'), Sides::new(1, 1, 1, 1));
398 let area = Rect::from_size(3, 3);
399 let mut pool = GraphemePool::new();
400 let mut frame = Frame::new(3, 3, &mut pool);
401 pad.render(area, &mut frame);
402
403 let lines = buf_to_lines(&frame.buffer);
404 assert_eq!(lines[0], " ");
405 assert_eq!(lines[1], " X ");
406 assert_eq!(lines[2], " ");
407 }
408
409 #[test]
410 fn naughty_widget_with_asymmetric_padding() {
411 let pad = Padding::new(Naughty, Sides::new(0, 0, 0, 2));
413 let area = Rect::from_size(5, 3);
414 let mut pool = GraphemePool::new();
415 let mut frame = Frame::new(5, 3, &mut pool);
416 pad.render(area, &mut frame);
417
418 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
420 assert_eq!(frame.buffer.get(2, 2).unwrap().content.as_char(), Some('Y'));
422 }
423}