sql_cli/ui/rendering/
cell_renderer.rs

1use ratatui::{
2    style::{Color, Modifier, Style},
3    widgets::{Block, Borders},
4};
5
6use crate::config::config::CellSelectionStyle;
7
8/// Different visual styles for rendering selected cells
9#[derive(Debug, Clone)]
10pub enum CellRenderMode {
11    /// Traditional underline style
12    Underline,
13    /// Full block/inverse video style
14    Block,
15    /// Border around the cell
16    Border,
17    /// Just corners of the cell
18    Corners,
19    /// Subtle highlight (just color change)
20    Subtle,
21}
22
23impl From<&str> for CellRenderMode {
24    fn from(s: &str) -> Self {
25        match s.to_lowercase().as_str() {
26            "block" => CellRenderMode::Block,
27            "border" => CellRenderMode::Border,
28            "corners" => CellRenderMode::Corners,
29            "subtle" => CellRenderMode::Subtle,
30            _ => CellRenderMode::Underline,
31        }
32    }
33}
34
35/// Renders a cell with the configured selection style
36pub struct CellRenderer {
37    style_config: CellSelectionStyle,
38}
39
40impl CellRenderer {
41    #[must_use]
42    pub fn new(style_config: CellSelectionStyle) -> Self {
43        Self { style_config }
44    }
45
46    /// Create a style for a selected cell based on configuration
47    #[must_use]
48    pub fn get_selected_style(&self) -> Style {
49        let mut style = Style::default();
50
51        // Parse foreground color
52        style = style.fg(self.parse_color(&self.style_config.foreground));
53
54        // Apply background if configured
55        if self.style_config.use_background {
56            style = style.bg(self.parse_color(&self.style_config.background));
57        }
58
59        // Apply modifiers based on mode
60        let mode = CellRenderMode::from(self.style_config.mode.as_str());
61        match mode {
62            CellRenderMode::Underline => {
63                if self.style_config.underline {
64                    style = style.add_modifier(Modifier::UNDERLINED);
65                }
66            }
67            CellRenderMode::Block => {
68                // Inverse video effect - swap foreground and background
69                style = style.add_modifier(Modifier::REVERSED);
70            }
71            CellRenderMode::Border | CellRenderMode::Corners => {
72                // Use dark gray background (like column highlight) with bright foreground
73                // This is easier on the eyes than cyan background
74                style = style
75                    .bg(Color::DarkGray) // Same as column mode background
76                    .fg(self.parse_color(&self.style_config.foreground)) // Use configured color
77                    .add_modifier(Modifier::BOLD);
78            }
79            CellRenderMode::Subtle => {
80                // Just use the color, no additional modifiers
81            }
82        }
83
84        // Apply bold if configured
85        if self.style_config.bold {
86            style = style.add_modifier(Modifier::BOLD);
87        }
88
89        style
90    }
91
92    /// Render a cell value with optional border/corner decorations
93    #[must_use]
94    pub fn render_cell_value(&self, value: &str, is_selected: bool, width: usize) -> String {
95        if !is_selected {
96            return value.to_string();
97        }
98
99        let mode = CellRenderMode::from(self.style_config.mode.as_str());
100
101        match mode {
102            CellRenderMode::Border => self.render_with_border(value, width),
103            CellRenderMode::Corners => self.render_with_corners(value, width),
104            _ => value.to_string(),
105        }
106    }
107
108    /// Render value with full border
109    fn render_with_border(&self, value: &str, width: usize) -> String {
110        let chars = match self.style_config.border_style.as_str() {
111            "double" => ('═', '║', '╔', '╗', '╚', '╝'),
112            "rounded" => ('─', '│', '╭', '╮', '╰', '╯'),
113            "thick" => ('━', '┃', '┏', '┓', '┗', '┛'),
114            _ => ('─', '│', '┌', '┐', '└', '┘'), // single
115        };
116
117        // For inline cell rendering, we can't really do multi-line borders
118        // So we'll just add subtle markers
119        format!("{}{}{}", chars.2, value, chars.3)
120    }
121
122    /// Render value with just corner markers
123    fn render_with_corners(&self, value: &str, width: usize) -> String {
124        let corners: Vec<char> = self.style_config.corner_chars.chars().collect();
125        if corners.len() >= 4 {
126            // Add subtle corner markers inline
127            format!("{}{}{}", corners[0], value, corners[1])
128        } else {
129            value.to_string()
130        }
131    }
132
133    /// Parse color string to Ratatui Color
134    fn parse_color(&self, color_str: &str) -> Color {
135        match color_str.to_lowercase().as_str() {
136            "black" => Color::Black,
137            "red" => Color::Red,
138            "green" => Color::Green,
139            "yellow" => Color::Yellow,
140            "blue" => Color::Blue,
141            "magenta" => Color::Magenta,
142            "cyan" => Color::Cyan,
143            "gray" | "grey" => Color::Gray,
144            "dark_gray" | "dark_grey" => Color::DarkGray,
145            "light_red" | "bright_red" => Color::LightRed,
146            "light_green" | "bright_green" => Color::LightGreen,
147            "light_yellow" | "bright_yellow" => Color::LightYellow,
148            "light_blue" | "bright_blue" => Color::LightBlue,
149            "light_magenta" | "bright_magenta" => Color::LightMagenta,
150            "light_cyan" | "bright_cyan" => Color::LightCyan,
151            "white" => Color::White,
152            "orange" => Color::Rgb(255, 165, 0),
153            "purple" => Color::Rgb(128, 0, 128),
154            "teal" => Color::Rgb(0, 128, 128),
155            "pink" => Color::Rgb(255, 192, 203),
156            _ => Color::Yellow, // Default
157        }
158    }
159
160    /// Get a preview of all available styles for configuration UI
161    #[must_use]
162    pub fn get_style_previews() -> Vec<(&'static str, &'static str)> {
163        vec![
164            ("underline", "Classic underline style"),
165            ("block", "Inverse/block selection"),
166            ("border", "Full border around cell"),
167            ("corners", "Corner markers only"),
168            ("subtle", "Just color highlight"),
169        ]
170    }
171}
172
173/// Helper to create bordered cell for special rendering
174#[must_use]
175pub fn create_bordered_cell<'a>(content: &str, border_style: &str) -> Block<'a> {
176    let block = Block::default().borders(Borders::ALL);
177
178    match border_style {
179        "double" => block.border_type(ratatui::widgets::BorderType::Double),
180        "rounded" => block.border_type(ratatui::widgets::BorderType::Rounded),
181        "thick" => block.border_type(ratatui::widgets::BorderType::Thick),
182        _ => block,
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_cell_render_modes() {
192        let config = CellSelectionStyle::default();
193        let renderer = CellRenderer::new(config);
194
195        let style = renderer.get_selected_style();
196        assert!(style.add_modifier.contains(Modifier::UNDERLINED));
197        assert!(style.add_modifier.contains(Modifier::BOLD));
198    }
199
200    #[test]
201    fn test_color_parsing() {
202        let mut config = CellSelectionStyle::default();
203        config.foreground = "orange".to_string();
204        let renderer = CellRenderer::new(config);
205
206        let style = renderer.get_selected_style();
207        // Orange should be RGB(255, 165, 0)
208        assert_eq!(style.fg, Some(Color::Rgb(255, 165, 0)));
209    }
210}