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