1use alloc::sync::Arc;
2use core::hash::{Hash, Hasher};
3use core::panic::{RefUnwindSafe, UnwindSafe};
4use core::{fmt, ptr};
5
6use ratatui_core::buffer::Buffer;
7use ratatui_core::layout::{Offset, Position, Rect};
8use ratatui_core::style::{Color, Modifier, Style, Styled};
9use ratatui_core::symbols::shade;
10use ratatui_core::widgets::Widget;
11
12#[derive(Debug, Clone, Eq)]
61pub struct Shadow {
62 effect: Effect,
63 style: Style,
64 offset: Offset,
65}
66
67#[derive(Debug, Clone)]
69enum Effect {
70 Overlay,
72 Symbol(&'static str),
74 Custom(Arc<dyn CellEffect>),
76}
77
78pub trait CellEffect: fmt::Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
86 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer);
88}
89
90impl Effect {
91 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
93 match self {
94 Self::Overlay => {}
95 Self::Symbol(symbol) => {
96 for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
97 buf[(x, y)].set_symbol(symbol);
98 });
99 }
100 Self::Custom(filter) => filter.apply(shadow_area, base_area, buf),
101 }
102 }
103}
104
105impl PartialEq for Effect {
106 fn eq(&self, other: &Self) -> bool {
107 match (self, other) {
108 (Self::Overlay, Self::Overlay) => true,
109 (Self::Symbol(lhs), Self::Symbol(rhs)) => lhs == rhs,
110 (Self::Custom(lhs), Self::Custom(rhs)) => Arc::ptr_eq(lhs, rhs),
111 _ => false,
112 }
113 }
114}
115
116impl Eq for Effect {}
117
118impl Hash for Effect {
119 fn hash<H: Hasher>(&self, state: &mut H) {
120 match self {
121 Self::Overlay => "overlay".hash(state),
122 Self::Symbol(symbol) => {
123 "symbol".hash(state);
124 symbol.hash(state);
125 }
126 Self::Custom(filter) => {
127 "custom".hash(state);
128 ptr::hash(Arc::as_ptr(filter), state);
129 }
130 }
131 }
132}
133
134impl PartialEq for Shadow {
135 fn eq(&self, other: &Self) -> bool {
136 self.effect == other.effect && self.style == other.style && self.offset == other.offset
137 }
138}
139
140impl Hash for Shadow {
141 fn hash<H: Hasher>(&self, state: &mut H) {
142 self.effect.hash(state);
143 self.style.hash(state);
144 self.offset.hash(state);
145 }
146}
147
148impl Shadow {
149 pub fn overlay() -> Self {
163 Self {
164 effect: Effect::Overlay,
165 style: Style::default(),
166 offset: Offset::new(1, 1),
167 }
168 }
169
170 pub fn block() -> Self {
180 Self::symbol(shade::FULL)
181 }
182
183 pub fn light_shade() -> Self {
193 Self::symbol(shade::LIGHT)
194 }
195
196 pub fn medium_shade() -> Self {
206 Self::symbol(shade::MEDIUM)
207 }
208
209 pub fn dark_shade() -> Self {
226 Self::symbol(shade::DARK)
227 }
228
229 pub fn symbol(symbol: &'static str) -> Self {
240 Self {
241 effect: Effect::Symbol(symbol),
242 style: Style::default(),
243 offset: Offset::new(1, 1),
244 }
245 }
246
247 pub fn custom<F: CellEffect + 'static>(effect: F) -> Self {
252 Self {
253 effect: Effect::Custom(Arc::new(effect)),
254 style: Style::default(),
255 offset: Offset::new(1, 1),
256 }
257 }
258
259 pub fn new<F: CellEffect + 'static>(effect: F) -> Self {
263 Self::custom(effect)
264 }
265
266 #[must_use]
268 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
269 self.style = style.into();
270 self
271 }
272
273 #[must_use]
278 pub const fn offset(mut self, offset: Offset) -> Self {
279 self.offset = offset;
280 self
281 }
282}
283
284impl Default for Shadow {
285 fn default() -> Self {
286 Self::overlay()
287 }
288}
289
290impl Styled for Shadow {
291 type Item = Self;
292
293 fn style(&self) -> Style {
294 self.style
295 }
296
297 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
298 self.style(style)
299 }
300}
301
302impl Widget for &Shadow {
303 fn render(self, area: Rect, buf: &mut Buffer) {
304 let shadow_area = area.offset(self.offset).intersection(buf.area);
305
306 for y in shadow_area.top()..shadow_area.bottom() {
308 for x in shadow_area.left()..shadow_area.right() {
309 if area.contains(Position { x, y }) {
310 continue;
311 }
312 buf[(x, y)].set_style(self.style);
313 }
314 }
315
316 self.effect.apply(shadow_area, area, buf);
318 }
319}
320
321#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash)]
326pub struct Dimmed;
327
328impl CellEffect for Dimmed {
329 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
330 for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
331 buf[(x, y)].modifier.insert(Modifier::DIM);
332 if let Color::Rgb(r, g, b) = buf[(x, y)].bg {
333 buf[(x, y)].bg = Color::Rgb(r / 2, g / 2, b / 2);
334 } else {
335 buf[(x, y)].bg = Color::Black;
336 }
337 });
338 }
339}
340
341pub const fn dimmed() -> Dimmed {
343 Dimmed
344}
345
346fn for_each_shadow_cell(
348 shadow_area: Rect,
349 base_area: Rect,
350 buf: &mut Buffer,
351 mut f: impl FnMut(u16, u16, &mut Buffer),
352) {
353 for y in shadow_area.top()..shadow_area.bottom() {
354 for x in shadow_area.left()..shadow_area.right() {
355 if base_area.contains(Position { x, y }) {
356 continue;
357 }
358 f(x, y, buf);
359 }
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use ratatui_core::buffer::Buffer;
366 use ratatui_core::layout::Rect;
367 use ratatui_core::style::{Color, Style};
368 use ratatui_core::widgets::Widget;
369 use rstest::rstest;
370
371 use super::*;
372
373 fn render_shadow(shadow: &Shadow) -> Buffer {
374 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 4));
375 shadow.render(Rect::new(0, 0, 2, 2), &mut buffer);
376 buffer
377 }
378
379 #[test]
380 fn overlay_renders_style_without_changing_symbols() {
381 let mut buffer = Buffer::with_lines(["abcd", "efgh", "ijkl", "mnop"]);
382 let shadow = Shadow::overlay().style(Style::new().red().on_blue());
383
384 (&shadow).render(Rect::new(0, 0, 2, 2), &mut buffer);
385
386 assert_eq!(buffer[(2, 1)].symbol(), "g");
387 assert_eq!(buffer[(1, 2)].symbol(), "j");
388 assert_eq!(buffer[(2, 2)].symbol(), "k");
389 assert_eq!(buffer[(2, 1)].fg, Color::Red);
390 assert_eq!(buffer[(2, 1)].bg, Color::Blue);
391 assert_eq!(buffer[(1, 1)].fg, Color::Reset);
392 assert_eq!(buffer[(1, 1)].bg, Color::Reset);
393 }
394
395 #[rstest]
396 #[case(Shadow::symbol("$"), "$")]
397 #[case(Shadow::block(), shade::FULL)]
398 fn symbol_filters_fill_only_visible_shadow_cells(
399 #[case] shadow: Shadow,
400 #[case] symbol: &'static str,
401 ) {
402 let buffer = render_shadow(&shadow);
403
404 assert_eq!(buffer[(2, 1)].symbol(), symbol);
405 assert_eq!(buffer[(1, 2)].symbol(), symbol);
406 assert_eq!(buffer[(2, 2)].symbol(), symbol);
407 assert_eq!(buffer[(1, 1)].symbol(), " ");
408 }
409
410 #[test]
411 fn render_is_clipped_to_buffer() {
412 let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 2));
413 let shadow = Shadow::symbol("#");
414
415 (&shadow).render(Rect::new(0, 0, 2, 1), &mut buffer);
416
417 assert_eq!(buffer[(2, 1)].symbol(), "#");
418 }
419
420 #[test]
421 fn custom_filter_is_applied() {
422 #[derive(Debug)]
423 struct PlusFilter;
424
425 impl CellEffect for PlusFilter {
426 fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
427 for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
428 buf[(x, y)].set_symbol("+");
429 });
430 }
431 }
432
433 let buffer = render_shadow(&Shadow::new(PlusFilter));
434
435 assert_eq!(buffer[(2, 1)].symbol(), "+");
436 assert_eq!(buffer[(1, 2)].symbol(), "+");
437 assert_eq!(buffer[(2, 2)].symbol(), "+");
438 }
439
440 #[test]
441 fn dimmed_filter_dims_background() {
442 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 4));
443 buffer.set_style(buffer.area, Style::new().bg(Color::Rgb(100, 120, 140)));
444 let shadow = Shadow::new(dimmed());
445
446 (&shadow).render(Rect::new(0, 0, 2, 2), &mut buffer);
447
448 assert!(buffer[(2, 1)].modifier.contains(Modifier::DIM));
449 assert_eq!(buffer[(2, 1)].bg, Color::Rgb(50, 60, 70));
450 assert_eq!(buffer[(1, 1)].bg, Color::Rgb(100, 120, 140));
451 assert!(!buffer[(1, 1)].modifier.contains(Modifier::DIM));
452 }
453}