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 #[must_use]
29 pub const fn padding(mut self, padding: Sides) -> Self {
30 self.padding = padding;
31 self
32 }
33
34 pub const fn padding_sides(&self) -> Sides {
36 self.padding
37 }
38
39 #[inline]
41 pub fn inner_area(&self, area: Rect) -> Rect {
42 area.inner(self.padding)
43 }
44
45 pub const fn inner(&self) -> &W {
47 &self.inner
48 }
49
50 pub fn inner_mut(&mut self) -> &mut W {
52 &mut self.inner
53 }
54
55 pub fn into_inner(self) -> W {
57 self.inner
58 }
59}
60
61struct ScissorGuard<'a, 'pool> {
62 frame: &'a mut Frame<'pool>,
63}
64
65impl<'a, 'pool> ScissorGuard<'a, 'pool> {
66 fn new(frame: &'a mut Frame<'pool>, rect: Rect) -> Self {
67 frame.buffer.push_scissor(rect);
68 Self { frame }
69 }
70}
71
72impl Drop for ScissorGuard<'_, '_> {
73 fn drop(&mut self) {
74 self.frame.buffer.pop_scissor();
75 }
76}
77
78impl<W: Widget> Widget for Padding<W> {
79 fn render(&self, area: Rect, frame: &mut Frame) {
80 #[cfg(feature = "tracing")]
81 let _span = tracing::debug_span!(
82 "widget_render",
83 widget = "Padding",
84 x = area.x,
85 y = area.y,
86 w = area.width,
87 h = area.height
88 )
89 .entered();
90
91 if area.is_empty() {
92 return;
93 }
94
95 for y in area.y..area.bottom() {
99 for x in area.x..area.right() {
100 if let Some(cell) = frame.buffer.get_mut(x, y) {
101 cell.content = ftui_render::cell::CellContent::EMPTY;
102 }
103 }
104 }
105
106 let inner = self.inner_area(area);
107 if inner.is_empty() {
108 return;
109 }
110
111 let guard = ScissorGuard::new(frame, inner);
112 self.inner.render(inner, guard.frame);
113 }
114
115 fn is_essential(&self) -> bool {
116 self.inner.is_essential()
117 }
118}
119
120impl<W: StatefulWidget> StatefulWidget for Padding<W> {
121 type State = W::State;
122
123 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
124 #[cfg(feature = "tracing")]
125 let _span = tracing::debug_span!(
126 "widget_render",
127 widget = "PaddingStateful",
128 x = area.x,
129 y = area.y,
130 w = area.width,
131 h = area.height
132 )
133 .entered();
134
135 if area.is_empty() {
136 return;
137 }
138
139 for y in area.y..area.bottom() {
140 for x in area.x..area.right() {
141 if let Some(cell) = frame.buffer.get_mut(x, y) {
142 cell.content = ftui_render::cell::CellContent::EMPTY;
143 }
144 }
145 }
146
147 let inner = self.inner_area(area);
148 if inner.is_empty() {
149 return;
150 }
151
152 let guard = ScissorGuard::new(frame, inner);
153 self.inner.render(inner, guard.frame, state);
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use ftui_render::buffer::Buffer;
161 use ftui_render::cell::Cell;
162 use ftui_render::grapheme_pool::GraphemePool;
163
164 fn buf_to_lines(buf: &Buffer) -> Vec<String> {
165 let mut lines = Vec::new();
166 for y in 0..buf.height() {
167 let mut row = String::with_capacity(buf.width() as usize);
168 for x in 0..buf.width() {
169 let ch = buf
170 .get(x, y)
171 .and_then(|c| c.content.as_char())
172 .unwrap_or(' ');
173 row.push(ch);
174 }
175 lines.push(row);
176 }
177 lines
178 }
179
180 #[derive(Debug, Clone, Copy)]
181 struct Fill(char);
182
183 impl Widget for Fill {
184 fn render(&self, area: Rect, frame: &mut Frame) {
185 for y in area.y..area.bottom() {
186 for x in area.x..area.right() {
187 frame.buffer.set(x, y, Cell::from_char(self.0));
188 }
189 }
190 }
191 }
192
193 #[derive(Debug, Clone, Copy)]
194 struct Naughty;
195
196 impl Widget for Naughty {
197 fn render(&self, _area: Rect, frame: &mut Frame) {
198 frame.buffer.set(0, 0, Cell::from_char('X'));
200 frame.buffer.set(2, 2, Cell::from_char('Y'));
201 }
202 }
203
204 #[derive(Debug, Clone, Copy)]
205 struct Boom;
206
207 impl Widget for Boom {
208 fn render(&self, _area: Rect, _frame: &mut Frame) {
209 unreachable!("boom");
210 }
211 }
212
213 #[test]
214 fn inner_area_zero_padding_is_identity() {
215 let pad = Padding::new(Fill('A'), Sides::all(0));
216 let area = Rect::new(3, 4, 10, 7);
217 assert_eq!(pad.inner_area(area), area);
218 }
219
220 #[test]
221 fn inner_area_asymmetric_padding() {
222 let pad = Padding::new(Fill('A'), Sides::new(1, 2, 1, 3));
223 let area = Rect::new(0, 0, 10, 4);
224 assert_eq!(pad.inner_area(area), Rect::new(3, 1, 5, 2));
225 }
226
227 #[test]
228 fn inner_area_clamps_when_padding_exceeds_area() {
229 let pad = Padding::new(Fill('A'), Sides::all(5));
230 let inner = pad.inner_area(Rect::new(0, 0, 2, 2));
231 assert_eq!(inner.width, 0);
232 assert_eq!(inner.height, 0);
233 }
234
235 #[test]
236 fn render_padding_shifts_child_and_leaves_gutter_blank() {
237 let pad = Padding::new(Fill('A'), 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_eq!(
244 buf_to_lines(&frame.buffer),
245 vec![
246 " ".to_string(),
247 " AAA ".to_string(),
248 " AAA ".to_string(),
249 " AAA ".to_string(),
250 " ".to_string(),
251 ]
252 );
253 }
254
255 #[test]
256 fn render_is_clipped_to_inner_rect_via_scissor() {
257 let pad = Padding::new(Naughty, Sides::all(1));
258 let area = Rect::from_size(5, 5);
259 let mut pool = GraphemePool::new();
260 let mut frame = Frame::new(5, 5, &mut pool);
261 pad.render(area, &mut frame);
262
263 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
265 assert_eq!(frame.buffer.get(2, 2).unwrap().content.as_char(), Some('Y'));
267 }
268
269 #[test]
270 fn scissor_stack_restores_on_panic() {
271 let pad = Padding::new(Boom, Sides::all(1));
272 let area = Rect::from_size(5, 5);
273 let mut pool = GraphemePool::new();
274 let mut frame = Frame::new(5, 5, &mut pool);
275 assert_eq!(frame.buffer.scissor_depth(), 1);
276
277 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
278 pad.render(area, &mut frame);
279 }));
280 assert!(result.is_err());
281 assert_eq!(frame.buffer.scissor_depth(), 1);
282 }
283
284 #[test]
285 fn render_empty_area_is_noop() {
286 let pad = Padding::new(Fill('X'), Sides::all(1));
287 let area = Rect::new(0, 0, 0, 0);
288 let mut pool = GraphemePool::new();
289 let mut frame = Frame::new(5, 5, &mut pool);
290 pad.render(area, &mut frame);
291 for y in 0..5 {
292 for x in 0..5u16 {
293 assert!(frame.buffer.get(x, y).unwrap().is_empty());
294 }
295 }
296 }
297
298 #[test]
299 fn padding_larger_than_area_renders_nothing() {
300 let pad = Padding::new(Fill('X'), Sides::all(10));
301 let area = Rect::from_size(5, 5);
302 let mut pool = GraphemePool::new();
303 let mut frame = Frame::new(5, 5, &mut pool);
304 pad.render(area, &mut frame);
305 for y in 0..5 {
307 for x in 0..5u16 {
308 assert!(frame.buffer.get(x, y).unwrap().is_empty());
309 }
310 }
311 }
312
313 #[test]
314 fn smaller_inner_area_clears_old_content() {
315 let area = Rect::from_size(5, 3);
316 let mut pool = GraphemePool::new();
317 let mut frame = Frame::new(5, 3, &mut pool);
318
319 Padding::new(Fill('X'), Sides::all(0)).render(area, &mut frame);
320 Padding::new(Fill('O'), Sides::all(1)).render(area, &mut frame);
321
322 assert_eq!(
323 buf_to_lines(&frame.buffer),
324 vec![
325 " ".to_string(),
326 " OOO ".to_string(),
327 " ".to_string()
328 ]
329 );
330 }
331
332 #[test]
333 fn empty_inner_area_clears_previous_content() {
334 let area = Rect::from_size(5, 3);
335 let mut pool = GraphemePool::new();
336 let mut frame = Frame::new(5, 3, &mut pool);
337
338 Padding::new(Fill('X'), Sides::all(0)).render(area, &mut frame);
339 Padding::new(Fill('O'), Sides::all(10)).render(area, &mut frame);
340
341 for y in 0..area.height {
342 for x in 0..area.width {
343 assert!(frame.buffer.get(x, y).unwrap().is_empty());
344 }
345 }
346 }
347
348 #[test]
349 fn asymmetric_padding_top_left() {
350 let pad = Padding::new(Fill('A'), Sides::new(2, 0, 0, 1));
351 let area = Rect::from_size(5, 5);
352 let mut pool = GraphemePool::new();
353 let mut frame = Frame::new(5, 5, &mut pool);
354 pad.render(area, &mut frame);
355
356 let lines = buf_to_lines(&frame.buffer);
357 assert_eq!(lines[0], " "); assert_eq!(lines[1], " "); assert_eq!(lines[2], " AAAA"); assert_eq!(lines[3], " AAAA");
362 assert_eq!(lines[4], " AAAA");
363 }
364
365 #[test]
366 fn padding_sides_accessor() {
367 let pad = Padding::new(Fill('A'), Sides::new(1, 2, 3, 4));
368 let s = pad.padding_sides();
369 assert_eq!(s.top, 1);
370 assert_eq!(s.right, 2);
371 assert_eq!(s.bottom, 3);
372 assert_eq!(s.left, 4);
373 }
374
375 #[test]
376 fn inner_accessor() {
377 let pad = Padding::new(Fill('A'), Sides::all(0));
378 assert_eq!(pad.inner().0, 'A');
379 }
380
381 #[test]
382 fn inner_mut_accessor() {
383 let mut pad = Padding::new(Fill('A'), Sides::all(0));
384 pad.inner_mut().0 = 'B';
385 assert_eq!(pad.inner().0, 'B');
386 }
387
388 #[test]
389 fn into_inner() {
390 let pad = Padding::new(Fill('Z'), Sides::all(0));
391 let inner = pad.into_inner();
392 assert_eq!(inner.0, 'Z');
393 }
394
395 #[test]
396 fn padding_builder() {
397 let pad = Padding::new(Fill('A'), Sides::all(0)).padding(Sides::all(2));
398 assert_eq!(pad.padding_sides(), Sides::all(2));
399 }
400
401 #[test]
402 fn is_essential_delegates_to_inner() {
403 #[derive(Debug, Clone, Copy)]
404 struct Essential;
405 impl Widget for Essential {
406 fn render(&self, _: Rect, _: &mut Frame) {}
407 fn is_essential(&self) -> bool {
408 true
409 }
410 }
411
412 let non_essential = Padding::new(Fill('A'), Sides::all(0));
413 assert!(!non_essential.is_essential());
414
415 let essential = Padding::new(Essential, Sides::all(0));
416 assert!(essential.is_essential());
417 }
418
419 #[test]
420 fn stateful_render_with_padding() {
421 #[derive(Debug, Clone, Copy)]
422 struct StateFill(char);
423
424 impl StatefulWidget for StateFill {
425 type State = usize;
426 fn render(&self, area: Rect, frame: &mut Frame, state: &mut usize) {
427 *state += 1;
428 for y in area.y..area.bottom() {
429 for x in area.x..area.right() {
430 frame.buffer.set(x, y, Cell::from_char(self.0));
431 }
432 }
433 }
434 }
435
436 let pad = Padding::new(StateFill('S'), Sides::all(1));
437 let area = Rect::from_size(5, 5);
438 let mut pool = GraphemePool::new();
439 let mut frame = Frame::new(5, 5, &mut pool);
440 let mut state: usize = 0;
441 StatefulWidget::render(&pad, area, &mut frame, &mut state);
442
443 assert_eq!(state, 1);
444 let lines = buf_to_lines(&frame.buffer);
445 assert_eq!(lines[0], " ");
446 assert_eq!(lines[1], " SSS ");
447 assert_eq!(lines[2], " SSS ");
448 }
449
450 #[test]
451 fn stateful_smaller_inner_area_clears_old_content() {
452 #[derive(Debug, Clone, Copy)]
453 struct StateFill(char);
454
455 impl StatefulWidget for StateFill {
456 type State = ();
457
458 fn render(&self, area: Rect, frame: &mut Frame, _state: &mut Self::State) {
459 for y in area.y..area.bottom() {
460 for x in area.x..area.right() {
461 frame.buffer.set(x, y, Cell::from_char(self.0));
462 }
463 }
464 }
465 }
466
467 let area = Rect::from_size(5, 3);
468 let mut pool = GraphemePool::new();
469 let mut frame = Frame::new(5, 3, &mut pool);
470 let mut state = ();
471
472 StatefulWidget::render(
473 &Padding::new(StateFill('X'), Sides::all(0)),
474 area,
475 &mut frame,
476 &mut state,
477 );
478 StatefulWidget::render(
479 &Padding::new(StateFill('O'), Sides::all(1)),
480 area,
481 &mut frame,
482 &mut state,
483 );
484
485 assert_eq!(
486 buf_to_lines(&frame.buffer),
487 vec![
488 " ".to_string(),
489 " OOO ".to_string(),
490 " ".to_string()
491 ]
492 );
493 }
494
495 #[test]
496 fn large_padding_single_cell_inner() {
497 let pad = Padding::new(Fill('X'), Sides::new(1, 1, 1, 1));
498 let area = Rect::from_size(3, 3);
499 let mut pool = GraphemePool::new();
500 let mut frame = Frame::new(3, 3, &mut pool);
501 pad.render(area, &mut frame);
502
503 let lines = buf_to_lines(&frame.buffer);
504 assert_eq!(lines[0], " ");
505 assert_eq!(lines[1], " X ");
506 assert_eq!(lines[2], " ");
507 }
508
509 #[test]
510 fn naughty_widget_with_asymmetric_padding() {
511 let pad = Padding::new(Naughty, Sides::new(0, 0, 0, 2));
513 let area = Rect::from_size(5, 3);
514 let mut pool = GraphemePool::new();
515 let mut frame = Frame::new(5, 3, &mut pool);
516 pad.render(area, &mut frame);
517
518 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
520 assert_eq!(frame.buffer.get(2, 2).unwrap().content.as_char(), Some('Y'));
522 }
523}