Skip to main content

ratatui_widgets/block/
shadow.rs

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/// A configurable shadow that can be rendered behind a [`Block`](crate::block::Block).
13///
14/// A [`Shadow`] is rendered in an offset area relative to the block. Its [`Style`] is applied
15/// first, then an optional cell effect can modify the affected cells, for example by filling them
16/// with a shading symbol or dimming the existing background.
17///
18/// Built-in presets:
19///
20/// - [`Shadow::overlay`] applies only style
21/// - [`Shadow::block`] fills with full block symbols
22/// - [`Shadow::light_shade`], [`Shadow::medium_shade`], and [`Shadow::dark_shade`] fill with shade
23///   symbols
24///
25/// ```plain
26/// ┌Popup─────┐
27/// │content   │▒
28/// └──────────┘▒
29///   ▒▒▒▒▒▒▒▒▒▒▒
30/// ```
31///
32/// # Custom effects
33///
34/// ```
35/// use ratatui::buffer::Buffer;
36/// use ratatui::layout::{Position, Rect};
37/// use ratatui::widgets::{Block, CellEffect, Shadow};
38///
39/// #[derive(Debug)]
40/// struct Checker;
41///
42/// impl CellEffect for Checker {
43///     fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
44///         for y in shadow_area.top()..shadow_area.bottom() {
45///             for x in shadow_area.left()..shadow_area.right() {
46///                 if base_area.contains(Position { x, y }) {
47///                     continue;
48///                 }
49///                 if (x + y) % 2 == 0 {
50///                     buf[(x, y)].set_symbol("░");
51///                 }
52///             }
53///         }
54///     }
55/// }
56///
57/// let shadow = Shadow::custom(Checker);
58/// let block = Block::bordered().shadow(shadow);
59/// ```
60#[derive(Debug, Clone, Eq)]
61pub struct Shadow {
62    effect: Effect,
63    style: Style,
64    offset: Offset,
65}
66
67/// The built-in shadow effects.
68#[derive(Debug, Clone)]
69enum Effect {
70    /// Applies no symbol changes and only keeps the shadow style.
71    Overlay,
72    /// Fills the shadow area with a single symbol.
73    Symbol(&'static str),
74    /// Applies a user-defined shadow effect.
75    Custom(Arc<dyn CellEffect>),
76}
77
78/// A cell effect that modifies the cells covered by a [`Shadow`].
79///
80/// Custom effects must keep the same auto-trait guarantees as the widgets that store them:
81/// [`Send`], [`Sync`], [`UnwindSafe`], and [`RefUnwindSafe`]. This preserves the thread-safety
82/// and panic-safety behavior of widgets that contain a [`Block`](crate::block::Block).
83///
84/// See [`Shadow::custom`] for how to create a shadow from a custom effect.
85pub trait CellEffect: fmt::Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
86    /// Applies the effect to the cells in `shadow_area`.
87    fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer);
88}
89
90impl Effect {
91    /// Applies the effect to the shadow area in the buffer.
92    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    /// Creates a shadow that only applies style to the offset area.
150    ///
151    /// This leaves the existing cell symbols unchanged.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use ratatui::style::Stylize;
157    /// use ratatui::widgets::{Block, Shadow};
158    ///
159    /// let shadow = Shadow::overlay().black().on_white();
160    /// let block = Block::bordered().shadow(shadow);
161    /// ```
162    pub fn overlay() -> Self {
163        Self {
164            effect: Effect::Overlay,
165            style: Style::default(),
166            offset: Offset::new(1, 1),
167        }
168    }
169
170    /// Creates a shadow filled with full block symbols.
171    ///
172    /// # Example
173    ///
174    /// ```
175    /// use ratatui::widgets::{Block, Shadow};
176    ///
177    /// let block = Block::bordered().shadow(Shadow::block());
178    /// ```
179    pub fn block() -> Self {
180        Self::symbol(shade::FULL)
181    }
182
183    /// Creates a shadow filled with light shade symbols.
184    ///
185    /// # Example
186    ///
187    /// ```
188    /// use ratatui::widgets::{Block, Shadow};
189    ///
190    /// let block = Block::bordered().shadow(Shadow::light_shade());
191    /// ```
192    pub fn light_shade() -> Self {
193        Self::symbol(shade::LIGHT)
194    }
195
196    /// Creates a shadow filled with medium shade symbols.
197    ///
198    /// # Example
199    ///
200    /// ```
201    /// use ratatui::widgets::{Block, Shadow};
202    ///
203    /// let block = Block::bordered().shadow(Shadow::medium_shade());
204    /// ```
205    pub fn medium_shade() -> Self {
206        Self::symbol(shade::MEDIUM)
207    }
208
209    /// Creates a shadow filled with dark shade symbols.
210    ///
211    /// # Example
212    ///
213    /// ```
214    /// use ratatui::layout::Offset;
215    /// use ratatui::style::Stylize;
216    /// use ratatui::widgets::{Block, Shadow};
217    ///
218    /// let block = Block::bordered().shadow(
219    ///     Shadow::dark_shade()
220    ///         .black()
221    ///         .on_white()
222    ///         .offset(Offset::new(2, 1)),
223    /// );
224    /// ```
225    pub fn dark_shade() -> Self {
226        Self::symbol(shade::DARK)
227    }
228
229    /// Creates a shadow filled with the given symbol.
230    ///
231    /// # Example
232    ///
233    /// ```
234    /// use ratatui::widgets::{Block, Shadow};
235    ///
236    /// let shadow = Shadow::symbol("░");
237    /// let block = Block::bordered().shadow(shadow);
238    /// ```
239    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    /// Creates a new shadow from a custom cell effect.
248    ///
249    /// The effect receives the shadow area, the original block area, and the target buffer. It is
250    /// called after the shadow style has been applied.
251    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    /// Creates a new shadow from a custom cell effect.
260    ///
261    /// Alias for [`Shadow::custom`].
262    pub fn new<F: CellEffect + 'static>(effect: F) -> Self {
263        Self::custom(effect)
264    }
265
266    /// Sets the style applied to the shadow area.
267    #[must_use]
268    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
269        self.style = style.into();
270        self
271    }
272
273    /// Sets the shadow offset relative to the original area.
274    ///
275    /// Positive horizontal values move the shadow to the right and positive vertical values move it
276    /// downward.
277    #[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        // Apply style
307        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        // Apply effect
317        self.effect.apply(shadow_area, area, buf);
318    }
319}
320
321/// A [`CellEffect`] that dims the shadow cells by setting the [`DIM`](Modifier::DIM) modifier.
322///
323/// If the cell background is RGB, each channel is halved. Otherwise the background is replaced
324/// with [`Color::Black`].
325#[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
341/// Creates a [`Dimmed`] shadow effect.
342pub const fn dimmed() -> Dimmed {
343    Dimmed
344}
345
346/// Helper for iterating over the shadow area while skipping cells that overlap the base area.
347fn 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}