Skip to main content

santui_core/widgets/
dim_overlay.rs

1use ratatui::buffer::Buffer;
2use ratatui::layout::Rect;
3use ratatui::style::Style;
4use ratatui::widgets::Widget;
5
6pub struct DimOverlay {
7    pub style: Style,
8}
9
10impl Widget for DimOverlay {
11    fn render(self, area: Rect, buf: &mut Buffer) {
12        let dim_bg = self.style.bg;
13        for y in area.top()..area.bottom() {
14            for x in area.left()..area.right() {
15                if let Some(cell) = buf.cell_mut((x, y)) {
16                    let mut s = cell.style();
17                    if let Some(bg) = dim_bg {
18                        // Cell::set_style normalises None → Some(Reset), so
19                        // check for either.
20                        if s.bg.is_none_or(|c| c == ratatui::style::Color::Reset) {
21                            s.bg = Some(bg);
22                        } else {
23                            s.bg = s.bg.map(|c| dim_color(c, DIM_FACTOR));
24                        }
25                    }
26                    s.fg = s.fg.map(|c| dim_color(c, DIM_FACTOR));
27                    cell.set_style(s);
28                }
29            }
30        }
31    }
32}
33
34/// Twin of `crates/ipc/src/render.rs` `DIM_FACTOR` — keep in sync.
35const DIM_FACTOR: f64 = 0.45;
36
37fn dim_color(c: ratatui::style::Color, factor: f64) -> ratatui::style::Color {
38    match c {
39        ratatui::style::Color::Rgb(r, g, b) => ratatui::style::Color::Rgb(
40            (r as f64 * factor) as u8,
41            (g as f64 * factor) as u8,
42            (b as f64 * factor) as u8,
43        ),
44        _ => c,
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use ratatui::style::{Color, Modifier};
52
53    fn style_fg(fg: Color) -> Style {
54        Style {
55            fg: Some(fg),
56            bg: Some(Color::Reset),
57            add_modifier: Modifier::empty(),
58            sub_modifier: Modifier::empty(),
59            underline_color: None,
60        }
61    }
62
63    fn style_fg_bg(fg: Color, bg: Color) -> Style {
64        Style {
65            fg: Some(fg),
66            bg: Some(bg),
67            add_modifier: Modifier::empty(),
68            sub_modifier: Modifier::empty(),
69            underline_color: None,
70        }
71    }
72
73    #[test]
74    fn dim_overlay_applies_overlay_bg_to_reset_cells() {
75        let area = Rect::new(0, 0, 3, 1);
76        let mut buf = Buffer::with_lines(vec!["abc"]);
77
78        DimOverlay {
79            style: Style::default().bg(Color::Rgb(10, 20, 30)),
80        }
81        .render(area, &mut buf);
82
83        // Default cells have fg/bg = Some(Reset) → treated as "no explicit bg"
84        let cell = buf.cell((0, 0)).unwrap();
85        assert_eq!(cell.style().fg, Some(Color::Reset)); // fg Reset stays Reset
86        assert_eq!(cell.style().bg, Some(Color::Rgb(10, 20, 30))); // overlay bg applied
87    }
88
89    #[test]
90    fn dim_overlay_dims_existing_rgb_fg_and_bg() {
91        let area = Rect::new(0, 0, 3, 1);
92        let mut buf = Buffer::with_lines(vec!["abc"]);
93        buf[(0u16, 0u16)].set_style(style_fg_bg(
94            Color::Rgb(200, 100, 50),
95            Color::Rgb(50, 100, 200),
96        ));
97
98        DimOverlay {
99            style: Style::default().bg(Color::Rgb(10, 20, 30)),
100        }
101        .render(area, &mut buf);
102
103        let cell = buf.cell((0, 0)).unwrap();
104        assert_eq!(cell.style().fg, Some(Color::Rgb(90, 45, 22)));
105        assert_eq!(cell.style().bg, Some(Color::Rgb(22, 45, 90)));
106    }
107
108    #[test]
109    fn dim_overlay_outside_area_untouched() {
110        let area = Rect::new(0, 0, 2, 1);
111        let mut buf = Buffer::with_lines(vec!["abc"]);
112        buf[(0u16, 0u16)].set_style(style_fg(Color::Rgb(200, 100, 50)));
113        buf[(2u16, 0u16)].set_style(style_fg(Color::Rgb(200, 100, 50)));
114
115        DimOverlay {
116            style: Style::default().bg(Color::Rgb(10, 20, 30)),
117        }
118        .render(area, &mut buf);
119
120        assert_eq!(
121            buf.cell((0, 0)).unwrap().style().fg,
122            Some(Color::Rgb(90, 45, 22))
123        );
124        assert_eq!(
125            buf.cell((2, 0)).unwrap().style().fg,
126            Some(Color::Rgb(200, 100, 50))
127        );
128    }
129
130    #[test]
131    fn dim_overlay_empty_area_no_panic() {
132        let area = Rect::new(0, 0, 0, 0);
133        let mut buf = Buffer::with_lines(vec!["abc"]);
134        DimOverlay {
135            style: Style::default().bg(Color::Rgb(10, 20, 30)),
136        }
137        .render(area, &mut buf);
138    }
139
140    #[test]
141    fn dim_overlay_no_overlay_bg_only_dims_fg() {
142        let area = Rect::new(0, 0, 3, 1);
143        let mut buf = Buffer::with_lines(vec!["abc"]);
144        buf[(0u16, 0u16)].set_style(style_fg(Color::Rgb(200, 100, 50)));
145
146        DimOverlay {
147            style: Style::default(), // no bg → dim_bg is None
148        }
149        .render(area, &mut buf);
150
151        let cell = buf.cell((0, 0)).unwrap();
152        assert_eq!(cell.style().fg, Some(Color::Rgb(90, 45, 22)));
153        assert_eq!(cell.style().bg, Some(Color::Reset));
154    }
155}