1use alloc::sync::Arc;
2use core::hash::{Hash, Hasher};
3use core::{fmt, ptr};
4
5use ratatui_core::buffer::Buffer;
6use ratatui_core::layout::{Offset, Position, Rect};
7use ratatui_core::style::{Color, Modifier, Style, Styled};
8use ratatui_core::symbols::shade;
9use ratatui_core::widgets::Widget;
10
11#[derive(Debug, Clone, Eq)]
60pub struct Shadow {
61 effect: Effect,
62 style: Style,
63 offset: Offset,
64}
65
66#[derive(Debug, Clone)]
68enum Effect {
69 Overlay,
71 Symbol(&'static str),
73 Custom(Arc<dyn CellEffect>),
75}
76
77pub trait CellEffect: fmt::Debug {
81 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer);
83}
84
85impl Effect {
86 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
88 match self {
89 Self::Overlay => {}
90 Self::Symbol(symbol) => {
91 for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
92 buf[(x, y)].set_symbol(symbol);
93 });
94 }
95 Self::Custom(filter) => filter.apply(shadow_area, base_area, buf),
96 }
97 }
98}
99
100impl PartialEq for Effect {
101 fn eq(&self, other: &Self) -> bool {
102 match (self, other) {
103 (Self::Overlay, Self::Overlay) => true,
104 (Self::Symbol(lhs), Self::Symbol(rhs)) => lhs == rhs,
105 (Self::Custom(lhs), Self::Custom(rhs)) => Arc::ptr_eq(lhs, rhs),
106 _ => false,
107 }
108 }
109}
110
111impl Eq for Effect {}
112
113impl Hash for Effect {
114 fn hash<H: Hasher>(&self, state: &mut H) {
115 match self {
116 Self::Overlay => "overlay".hash(state),
117 Self::Symbol(symbol) => {
118 "symbol".hash(state);
119 symbol.hash(state);
120 }
121 Self::Custom(filter) => {
122 "custom".hash(state);
123 ptr::hash(Arc::as_ptr(filter), state);
124 }
125 }
126 }
127}
128
129impl PartialEq for Shadow {
130 fn eq(&self, other: &Self) -> bool {
131 self.effect == other.effect && self.style == other.style && self.offset == other.offset
132 }
133}
134
135impl Hash for Shadow {
136 fn hash<H: Hasher>(&self, state: &mut H) {
137 self.effect.hash(state);
138 self.style.hash(state);
139 self.offset.hash(state);
140 }
141}
142
143impl Shadow {
144 pub fn overlay() -> Self {
158 Self {
159 effect: Effect::Overlay,
160 style: Style::default(),
161 offset: Offset::new(1, 1),
162 }
163 }
164
165 pub fn block() -> Self {
175 Self::symbol(shade::FULL)
176 }
177
178 pub fn light_shade() -> Self {
188 Self::symbol(shade::LIGHT)
189 }
190
191 pub fn medium_shade() -> Self {
201 Self::symbol(shade::MEDIUM)
202 }
203
204 pub fn dark_shade() -> Self {
221 Self::symbol(shade::DARK)
222 }
223
224 pub fn symbol(symbol: &'static str) -> Self {
235 Self {
236 effect: Effect::Symbol(symbol),
237 style: Style::default(),
238 offset: Offset::new(1, 1),
239 }
240 }
241
242 pub fn custom<F: CellEffect + 'static>(effect: F) -> Self {
247 Self {
248 effect: Effect::Custom(Arc::new(effect)),
249 style: Style::default(),
250 offset: Offset::new(1, 1),
251 }
252 }
253
254 pub fn new<F: CellEffect + 'static>(effect: F) -> Self {
258 Self::custom(effect)
259 }
260
261 #[must_use]
263 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
264 self.style = style.into();
265 self
266 }
267
268 #[must_use]
273 pub const fn offset(mut self, offset: Offset) -> Self {
274 self.offset = offset;
275 self
276 }
277}
278
279impl Default for Shadow {
280 fn default() -> Self {
281 Self::overlay()
282 }
283}
284
285impl Styled for Shadow {
286 type Item = Self;
287
288 fn style(&self) -> Style {
289 self.style
290 }
291
292 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
293 self.style(style)
294 }
295}
296
297impl Widget for &Shadow {
298 fn render(self, area: Rect, buf: &mut Buffer) {
299 let shadow_area = area.offset(self.offset).intersection(buf.area);
300
301 for y in shadow_area.top()..shadow_area.bottom() {
303 for x in shadow_area.left()..shadow_area.right() {
304 if area.contains(Position { x, y }) {
305 continue;
306 }
307 buf[(x, y)].set_style(self.style);
308 }
309 }
310
311 self.effect.apply(shadow_area, area, buf);
313 }
314}
315
316#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash)]
321pub struct Dimmed;
322
323impl CellEffect for Dimmed {
324 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
325 for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
326 buf[(x, y)].modifier.insert(Modifier::DIM);
327 if let Color::Rgb(r, g, b) = buf[(x, y)].bg {
328 buf[(x, y)].bg = Color::Rgb(r / 2, g / 2, b / 2);
329 } else {
330 buf[(x, y)].bg = Color::Black;
331 }
332 });
333 }
334}
335
336pub const fn dimmed() -> Dimmed {
338 Dimmed
339}
340
341fn for_each_shadow_cell(
343 shadow_area: Rect,
344 base_area: Rect,
345 buf: &mut Buffer,
346 mut f: impl FnMut(u16, u16, &mut Buffer),
347) {
348 for y in shadow_area.top()..shadow_area.bottom() {
349 for x in shadow_area.left()..shadow_area.right() {
350 if base_area.contains(Position { x, y }) {
351 continue;
352 }
353 f(x, y, buf);
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use ratatui_core::buffer::Buffer;
361 use ratatui_core::layout::Rect;
362 use ratatui_core::style::{Color, Style};
363 use ratatui_core::widgets::Widget;
364 use rstest::rstest;
365
366 use super::*;
367
368 fn render_shadow(shadow: &Shadow) -> Buffer {
369 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 4));
370 shadow.render(Rect::new(0, 0, 2, 2), &mut buffer);
371 buffer
372 }
373
374 #[test]
375 fn overlay_renders_style_without_changing_symbols() {
376 let mut buffer = Buffer::with_lines(["abcd", "efgh", "ijkl", "mnop"]);
377 let shadow = Shadow::overlay().style(Style::new().red().on_blue());
378
379 (&shadow).render(Rect::new(0, 0, 2, 2), &mut buffer);
380
381 assert_eq!(buffer[(2, 1)].symbol(), "g");
382 assert_eq!(buffer[(1, 2)].symbol(), "j");
383 assert_eq!(buffer[(2, 2)].symbol(), "k");
384 assert_eq!(buffer[(2, 1)].fg, Color::Red);
385 assert_eq!(buffer[(2, 1)].bg, Color::Blue);
386 assert_eq!(buffer[(1, 1)].fg, Color::Reset);
387 assert_eq!(buffer[(1, 1)].bg, Color::Reset);
388 }
389
390 #[rstest]
391 #[case(Shadow::symbol("$"), "$")]
392 #[case(Shadow::block(), shade::FULL)]
393 fn symbol_filters_fill_only_visible_shadow_cells(
394 #[case] shadow: Shadow,
395 #[case] symbol: &'static str,
396 ) {
397 let buffer = render_shadow(&shadow);
398
399 assert_eq!(buffer[(2, 1)].symbol(), symbol);
400 assert_eq!(buffer[(1, 2)].symbol(), symbol);
401 assert_eq!(buffer[(2, 2)].symbol(), symbol);
402 assert_eq!(buffer[(1, 1)].symbol(), " ");
403 }
404
405 #[test]
406 fn render_is_clipped_to_buffer() {
407 let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 2));
408 let shadow = Shadow::symbol("#");
409
410 (&shadow).render(Rect::new(0, 0, 2, 1), &mut buffer);
411
412 assert_eq!(buffer[(2, 1)].symbol(), "#");
413 }
414
415 #[test]
416 fn custom_filter_is_applied() {
417 #[derive(Debug)]
418 struct PlusFilter;
419
420 impl CellEffect for PlusFilter {
421 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
422 for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
423 buf[(x, y)].set_symbol("+");
424 });
425 }
426 }
427
428 let buffer = render_shadow(&Shadow::new(PlusFilter));
429
430 assert_eq!(buffer[(2, 1)].symbol(), "+");
431 assert_eq!(buffer[(1, 2)].symbol(), "+");
432 assert_eq!(buffer[(2, 2)].symbol(), "+");
433 }
434
435 #[test]
436 fn dimmed_filter_dims_background() {
437 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 4));
438 buffer.set_style(buffer.area, Style::new().bg(Color::Rgb(100, 120, 140)));
439 let shadow = Shadow::new(dimmed());
440
441 (&shadow).render(Rect::new(0, 0, 2, 2), &mut buffer);
442
443 assert!(buffer[(2, 1)].modifier.contains(Modifier::DIM));
444 assert_eq!(buffer[(2, 1)].bg, Color::Rgb(50, 60, 70));
445 assert_eq!(buffer[(1, 1)].bg, Color::Rgb(100, 120, 140));
446 assert!(!buffer[(1, 1)].modifier.contains(Modifier::DIM));
447 }
448}