use alloc::sync::Arc;
use core::hash::{Hash, Hasher};
use core::{fmt, ptr};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Offset, Position, Rect};
use ratatui_core::style::{Color, Modifier, Style, Styled};
use ratatui_core::symbols::shade;
use ratatui_core::widgets::Widget;
#[derive(Debug, Clone, Eq)]
pub struct Shadow {
effect: Effect,
style: Style,
offset: Offset,
}
#[derive(Debug, Clone)]
enum Effect {
Overlay,
Symbol(&'static str),
Custom(Arc<dyn CellEffect>),
}
pub trait CellEffect: fmt::Debug {
fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer);
}
impl Effect {
fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
match self {
Self::Overlay => {}
Self::Symbol(symbol) => {
for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
buf[(x, y)].set_symbol(symbol);
});
}
Self::Custom(filter) => filter.apply(shadow_area, base_area, buf),
}
}
}
impl PartialEq for Effect {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Overlay, Self::Overlay) => true,
(Self::Symbol(lhs), Self::Symbol(rhs)) => lhs == rhs,
(Self::Custom(lhs), Self::Custom(rhs)) => Arc::ptr_eq(lhs, rhs),
_ => false,
}
}
}
impl Eq for Effect {}
impl Hash for Effect {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Overlay => "overlay".hash(state),
Self::Symbol(symbol) => {
"symbol".hash(state);
symbol.hash(state);
}
Self::Custom(filter) => {
"custom".hash(state);
ptr::hash(Arc::as_ptr(filter), state);
}
}
}
}
impl PartialEq for Shadow {
fn eq(&self, other: &Self) -> bool {
self.effect == other.effect && self.style == other.style && self.offset == other.offset
}
}
impl Hash for Shadow {
fn hash<H: Hasher>(&self, state: &mut H) {
self.effect.hash(state);
self.style.hash(state);
self.offset.hash(state);
}
}
impl Shadow {
pub fn overlay() -> Self {
Self {
effect: Effect::Overlay,
style: Style::default(),
offset: Offset::new(1, 1),
}
}
pub fn block() -> Self {
Self::symbol(shade::FULL)
}
pub fn light_shade() -> Self {
Self::symbol(shade::LIGHT)
}
pub fn medium_shade() -> Self {
Self::symbol(shade::MEDIUM)
}
pub fn dark_shade() -> Self {
Self::symbol(shade::DARK)
}
pub fn symbol(symbol: &'static str) -> Self {
Self {
effect: Effect::Symbol(symbol),
style: Style::default(),
offset: Offset::new(1, 1),
}
}
pub fn custom<F: CellEffect + 'static>(effect: F) -> Self {
Self {
effect: Effect::Custom(Arc::new(effect)),
style: Style::default(),
offset: Offset::new(1, 1),
}
}
pub fn new<F: CellEffect + 'static>(effect: F) -> Self {
Self::custom(effect)
}
#[must_use]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use]
pub const fn offset(mut self, offset: Offset) -> Self {
self.offset = offset;
self
}
}
impl Default for Shadow {
fn default() -> Self {
Self::overlay()
}
}
impl Styled for Shadow {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Widget for &Shadow {
fn render(self, area: Rect, buf: &mut Buffer) {
let shadow_area = area.offset(self.offset).intersection(buf.area);
for y in shadow_area.top()..shadow_area.bottom() {
for x in shadow_area.left()..shadow_area.right() {
if area.contains(Position { x, y }) {
continue;
}
buf[(x, y)].set_style(self.style);
}
}
self.effect.apply(shadow_area, area, buf);
}
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash)]
pub struct Dimmed;
impl CellEffect for Dimmed {
fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
buf[(x, y)].modifier.insert(Modifier::DIM);
if let Color::Rgb(r, g, b) = buf[(x, y)].bg {
buf[(x, y)].bg = Color::Rgb(r / 2, g / 2, b / 2);
} else {
buf[(x, y)].bg = Color::Black;
}
});
}
}
pub const fn dimmed() -> Dimmed {
Dimmed
}
fn for_each_shadow_cell(
shadow_area: Rect,
base_area: Rect,
buf: &mut Buffer,
mut f: impl FnMut(u16, u16, &mut Buffer),
) {
for y in shadow_area.top()..shadow_area.bottom() {
for x in shadow_area.left()..shadow_area.right() {
if base_area.contains(Position { x, y }) {
continue;
}
f(x, y, buf);
}
}
}
#[cfg(test)]
mod tests {
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::{Color, Style};
use ratatui_core::widgets::Widget;
use rstest::rstest;
use super::*;
fn render_shadow(shadow: &Shadow) -> Buffer {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 4));
shadow.render(Rect::new(0, 0, 2, 2), &mut buffer);
buffer
}
#[test]
fn overlay_renders_style_without_changing_symbols() {
let mut buffer = Buffer::with_lines(["abcd", "efgh", "ijkl", "mnop"]);
let shadow = Shadow::overlay().style(Style::new().red().on_blue());
(&shadow).render(Rect::new(0, 0, 2, 2), &mut buffer);
assert_eq!(buffer[(2, 1)].symbol(), "g");
assert_eq!(buffer[(1, 2)].symbol(), "j");
assert_eq!(buffer[(2, 2)].symbol(), "k");
assert_eq!(buffer[(2, 1)].fg, Color::Red);
assert_eq!(buffer[(2, 1)].bg, Color::Blue);
assert_eq!(buffer[(1, 1)].fg, Color::Reset);
assert_eq!(buffer[(1, 1)].bg, Color::Reset);
}
#[rstest]
#[case(Shadow::symbol("$"), "$")]
#[case(Shadow::block(), shade::FULL)]
fn symbol_filters_fill_only_visible_shadow_cells(
#[case] shadow: Shadow,
#[case] symbol: &'static str,
) {
let buffer = render_shadow(&shadow);
assert_eq!(buffer[(2, 1)].symbol(), symbol);
assert_eq!(buffer[(1, 2)].symbol(), symbol);
assert_eq!(buffer[(2, 2)].symbol(), symbol);
assert_eq!(buffer[(1, 1)].symbol(), " ");
}
#[test]
fn render_is_clipped_to_buffer() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 2));
let shadow = Shadow::symbol("#");
(&shadow).render(Rect::new(0, 0, 2, 1), &mut buffer);
assert_eq!(buffer[(2, 1)].symbol(), "#");
}
#[test]
fn custom_filter_is_applied() {
#[derive(Debug)]
struct PlusFilter;
impl CellEffect for PlusFilter {
fn apply(&self, shadow_area: Rect, base_area: Rect, buf: &mut Buffer) {
for_each_shadow_cell(shadow_area, base_area, buf, |x, y, buf| {
buf[(x, y)].set_symbol("+");
});
}
}
let buffer = render_shadow(&Shadow::new(PlusFilter));
assert_eq!(buffer[(2, 1)].symbol(), "+");
assert_eq!(buffer[(1, 2)].symbol(), "+");
assert_eq!(buffer[(2, 2)].symbol(), "+");
}
#[test]
fn dimmed_filter_dims_background() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 4));
buffer.set_style(buffer.area, Style::new().bg(Color::Rgb(100, 120, 140)));
let shadow = Shadow::new(dimmed());
(&shadow).render(Rect::new(0, 0, 2, 2), &mut buffer);
assert!(buffer[(2, 1)].modifier.contains(Modifier::DIM));
assert_eq!(buffer[(2, 1)].bg, Color::Rgb(50, 60, 70));
assert_eq!(buffer[(1, 1)].bg, Color::Rgb(100, 120, 140));
assert!(!buffer[(1, 1)].modifier.contains(Modifier::DIM));
}
}