Skip to main content

hefesto_widgets/
styles.rs

1use ratatui::buffer::Buffer;
2use ratatui::layout::Rect;
3use ratatui::style::{Color, Modifier, Style};
4use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation, ScrollbarState};
5
6use crate::popup::PopupSize;
7
8// ── Colours ──────────────────────────────────────────
9
10pub const BORDER_GRAY: Color = Color::Rgb(100, 100, 100);
11pub const OUTPUT_GRAY: Color = Color::Rgb(120, 120, 120);
12pub const FOOTER_GRAY: Color = Color::Rgb(140, 140, 140);
13
14// ── Symbols ──────────────────────────────────────────
15
16pub const CHECK: &str = "✓";
17pub const CROSS: &str = "✗";
18
19pub const HIGHLIGHT_SYMBOL: &str = "> ";
20
21pub const BACKGROUND_DOT_SYMBOL: &str = "+";
22
23// ── Dimensions ───────────────────────────────────────
24
25pub const POPUP_WIDTH: PopupSize = PopupSize::Fixed(44);
26
27// ── Styles ───────────────────────────────────────────
28
29pub const DEFAULT_HIGHLIGHT: Style = Style::new()
30    .fg(Color::Blue)
31    .bg(Color::Black)
32    .add_modifier(Modifier::REVERSED);
33
34// ── Padding ──────────────────────────────────────────
35
36pub const TEXT_PADDING: Padding = Padding::new(1, 1, 0, 0);
37
38// ── Frames ───────────────────────────────────────────
39
40pub const DEFAULT_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
41
42// ── Dot pattern ──────────────────────────────────────
43
44/// Distribution pattern for the background dot grid.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum DotPattern {
47    /// Diagonal: `(x + y) % density == 0`. Organic, non-rigid.
48    Diagonal,
49    /// Grid: `x % density == 0 && y % density == 0`. Structured.
50    #[default]
51    Grid,
52}
53
54// ── Helpers ──────────────────────────────────────────
55
56/// Paints a dot pattern on empty cells in the given area.
57///
58/// Only affects cells whose symbol is `" "`. Useful as background
59/// decoration outside widgets.
60///
61/// To use as a decorative background outside popups:
62/// ```no_run
63/// # use ratatui::layout::Rect;
64/// # use ratatui::style::Color;
65/// # use hefesto_widgets::decorate_with_dots;
66/// # fn example(f: &mut ratatui::Frame) {
67/// let area = f.area();
68/// decorate_with_dots(f.buffer_mut(), area, Color::DarkGray, "\u{00B7}", 4);
69/// # }
70/// ```
71///
72/// By default the pattern is grid: `x % density == 0 && y % density == 0`.
73/// For a different pattern use [`decorate_with_dots_with_pattern`].
74///
75/// With `density = 1` every cell is painted, with `density = 4` roughly 1/4
76/// of cells, with `density = 10` very sparse.
77///
78/// This is opt-in: simply don't call it to have no decoration at all.
79pub fn decorate_with_dots(buf: &mut Buffer, area: Rect, color: Color, symbol: &str, density: u16) {
80    decorate_with_dots_with_pattern(buf, area, color, symbol, density, density, DotPattern::default());
81}
82
83/// Same as [`decorate_with_dots`] but with configurable [`DotPattern`].
84pub fn decorate_with_dots_with_pattern(
85    buf: &mut Buffer,
86    area: Rect,
87    color: Color,
88    symbol: &str,
89    density_x: u16,
90    density_y: u16,
91    pattern: DotPattern,
92) {
93    if density_x == 0 || density_y == 0 {
94        return;
95    }
96    for y in area.y..area.bottom() {
97        for x in area.x..area.right() {
98            let hit = match pattern {
99                DotPattern::Diagonal => (x + y) % density_x == 0,
100                DotPattern::Grid => x % density_x == 0 && y % density_y == 0,
101            };
102            if !hit {
103                continue;
104            }
105            let cell = &mut buf[(x, y)];
106            if cell.symbol() == " " {
107                cell.set_symbol(symbol);
108                cell.set_fg(color);
109            }
110        }
111    }
112}
113
114pub fn default_scrollbar() -> Scrollbar<'static> {
115    Scrollbar::new(ScrollbarOrientation::VerticalRight)
116        .begin_symbol(None)
117        .end_symbol(None)
118}
119
120pub fn default_scrollbar_state(total: usize, position: usize) -> ScrollbarState {
121    ScrollbarState::new(total).position(position)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn decorate_with_dots_paints_empty_cells() {
130        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
131        decorate_with_dots(&mut buf, Rect::new(0, 0, 10, 5), Color::Red, "·", 2);
132        // Grid with density=2: x%2==0 AND y%2==0
133        assert_eq!(buf[(0, 0)].symbol(), "·");
134        assert_eq!(buf[(0, 0)].fg, Color::Red);
135        assert_eq!(buf[(2, 0)].symbol(), "·");
136        assert_eq!(buf[(0, 2)].symbol(), "·");
137        assert_eq!(buf[(2, 2)].symbol(), "·");
138        // Diagonal positions (1,1) not painted in grid
139        assert_eq!(buf[(1, 0)].symbol(), " ");
140        assert_eq!(buf[(0, 1)].symbol(), " ");
141        assert_eq!(buf[(1, 1)].symbol(), " ");
142    }
143
144    #[test]
145    fn decorate_with_dots_skips_non_empty_cells() {
146        let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
147        buf[(2, 0)].set_symbol("X");
148        decorate_with_dots(&mut buf, Rect::new(0, 0, 5, 1), Color::Blue, "·", 1);
149        assert_eq!(buf[(2, 0)].symbol(), "X");
150        assert_eq!(buf[(0, 0)].symbol(), "·");
151        assert_eq!(buf[(3, 0)].symbol(), "·");
152    }
153
154    #[test]
155    fn decorate_with_dots_density_5() {
156        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
157        decorate_with_dots(&mut buf, Rect::new(0, 0, 10, 1), Color::White, "·", 5);
158        assert_eq!(buf[(0, 0)].symbol(), "·");
159        assert_eq!(buf[(5, 0)].symbol(), "·");
160        assert_eq!(buf[(1, 0)].symbol(), " ");
161        assert_eq!(buf[(3, 0)].symbol(), " ");
162    }
163
164    #[test]
165    fn decorate_with_dots_respects_area_bounds() {
166        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
167        decorate_with_dots(&mut buf, Rect::new(5, 5, 3, 3), Color::Green, "·", 1);
168        assert_eq!(buf[(5, 5)].symbol(), "·");
169        assert_eq!(buf[(7, 7)].symbol(), "·");
170        assert_eq!(buf[(0, 0)].symbol(), " ");
171        assert_eq!(buf[(8, 5)].symbol(), " ");
172    }
173
174    #[test]
175    fn decorate_with_dots_density_zero_is_noop() {
176        let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
177        decorate_with_dots(&mut buf, Rect::new(0, 0, 5, 1), Color::Red, "·", 0);
178        assert_eq!(buf[(0, 0)].symbol(), " ");
179    }
180
181    #[test]
182    fn decorate_with_dots_custom_symbol() {
183        let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
184        decorate_with_dots(&mut buf, Rect::new(0, 0, 3, 1), Color::Cyan, "•", 1);
185        assert_eq!(buf[(0, 0)].symbol(), "•");
186        assert_eq!(buf[(0, 0)].fg, Color::Cyan);
187    }
188
189    // ── patterns ──
190
191    #[test]
192    fn decorate_with_dots_grid_pattern() {
193        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
194        decorate_with_dots_with_pattern(
195            &mut buf, Rect::new(0, 0, 10, 5), Color::Red, "·", 4, 2, DotPattern::Grid,
196        );
197        assert_eq!(buf[(0, 0)].symbol(), "·");
198        assert_eq!(buf[(4, 0)].symbol(), "·");
199        assert_eq!(buf[(0, 4)].symbol(), "·");
200        assert_eq!(buf[(4, 4)].symbol(), "·");
201        assert_eq!(buf[(1, 1)].symbol(), " ");
202        assert_eq!(buf[(2, 2)].symbol(), " ");
203    }
204
205    #[test]
206    fn decorate_with_dots_default_is_grid() {
207        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
208        decorate_with_dots(&mut buf, Rect::new(0, 0, 4, 1), Color::White, "·", 2);
209        // Grid: x%2==0 AND y%2==0. Con y=0, solo x%2==0
210        assert_eq!(buf[(0, 0)].symbol(), "·");
211        assert_eq!(buf[(2, 0)].symbol(), "·");
212        assert_eq!(buf[(1, 0)].symbol(), " ");
213        assert_eq!(buf[(3, 0)].symbol(), " ");
214    }
215
216    #[test]
217    fn dot_pattern_default_is_grid() {
218        assert_eq!(DotPattern::default(), DotPattern::Grid);
219    }
220
221    #[test]
222    fn decorate_with_dots_color_persists() {
223        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
224        decorate_with_dots(&mut buf, Rect::new(0, 0, 4, 1), Color::Cyan, "·", 2);
225        assert_eq!(buf[(0, 0)].fg, Color::Cyan);
226    }
227}