Skip to main content

ratatui_interact/components/hotkey_dialog/
style.rs

1//! Style configuration for the hotkey dialog.
2//!
3//! This module provides customizable styling options for the hotkey dialog component.
4
5use ratatui::style::{Color, Modifier, Style};
6
7/// Style configuration for the hotkey dialog.
8#[derive(Debug, Clone)]
9pub struct HotkeyDialogStyle {
10    /// Dialog title
11    pub title: String,
12    /// Border color when focused
13    pub border_focused: Color,
14    /// Border color when not focused
15    pub border_unfocused: Color,
16    /// Title color
17    pub title_color: Color,
18    /// Color for global hotkeys
19    pub global_key_color: Color,
20    /// Color for non-global hotkeys
21    pub local_key_color: Color,
22    /// Background color for selected items
23    pub selected_bg: Color,
24    /// Text color for selected items
25    pub selected_fg: Color,
26    /// Color for locked/non-customizable indicator
27    pub locked_color: Color,
28    /// Color for the cursor in search field
29    pub cursor_color: Color,
30    /// Placeholder text color
31    pub placeholder_color: Color,
32    /// Default text color
33    pub text_color: Color,
34    /// Dimmed text color
35    pub dim_color: Color,
36    /// Width percentage (0-100)
37    pub width_percent: u16,
38    /// Height percentage (0-100)
39    pub height_percent: u16,
40    /// Maximum width in columns
41    pub max_width: u16,
42    /// Maximum height in rows
43    pub max_height: u16,
44    /// Minimum width in columns
45    pub min_width: u16,
46    /// Minimum height in rows
47    pub min_height: u16,
48    /// Category list width percentage (0-100)
49    pub category_width_percent: u16,
50    /// Search bar height
51    pub search_height: u16,
52    /// Footer height
53    pub footer_height: u16,
54    /// Global context indicator (displays before global hotkeys, default: "G")
55    pub global_indicator: String,
56    /// Locked/non-customizable indicator (e.g., "L")
57    pub locked_indicator: String,
58    /// Search placeholder text
59    pub search_placeholder: String,
60}
61
62impl Default for HotkeyDialogStyle {
63    fn default() -> Self {
64        Self {
65            title: " Hotkey Configuration ".to_string(),
66            border_focused: Color::Yellow,
67            border_unfocused: Color::DarkGray,
68            title_color: Color::Yellow,
69            global_key_color: Color::Yellow,
70            local_key_color: Color::Cyan,
71            selected_bg: Color::Yellow,
72            selected_fg: Color::Black,
73            locked_color: Color::Red,
74            cursor_color: Color::Yellow,
75            placeholder_color: Color::DarkGray,
76            text_color: Color::White,
77            dim_color: Color::DarkGray,
78            width_percent: 85,
79            height_percent: 85,
80            max_width: 110,
81            max_height: 45,
82            min_width: 70,
83            min_height: 25,
84            category_width_percent: 28,
85            search_height: 3,
86            footer_height: 2,
87            global_indicator: "[G]".to_string(),
88            locked_indicator: "L".to_string(),
89            search_placeholder: "Type to filter hotkeys...".to_string(),
90        }
91    }
92}
93
94impl From<&crate::theme::Theme> for HotkeyDialogStyle {
95    fn from(theme: &crate::theme::Theme) -> Self {
96        let p = &theme.palette;
97        Self {
98            title: " Hotkey Configuration ".to_string(),
99            border_focused: p.border_focused,
100            border_unfocused: p.border_disabled,
101            title_color: p.primary,
102            global_key_color: p.primary,
103            local_key_color: p.secondary,
104            selected_bg: p.highlight_bg,
105            selected_fg: p.highlight_fg,
106            locked_color: p.error,
107            cursor_color: p.primary,
108            placeholder_color: p.text_placeholder,
109            text_color: p.text,
110            dim_color: p.text_disabled,
111            width_percent: 85,
112            height_percent: 85,
113            max_width: 110,
114            max_height: 45,
115            min_width: 70,
116            min_height: 25,
117            category_width_percent: 28,
118            search_height: 3,
119            footer_height: 2,
120            global_indicator: "[G]".to_string(),
121            locked_indicator: "L".to_string(),
122            search_placeholder: "Type to filter hotkeys...".to_string(),
123        }
124    }
125}
126
127impl HotkeyDialogStyle {
128    /// Create a new style with default values.
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Set the dialog title.
134    pub fn title(mut self, title: impl Into<String>) -> Self {
135        self.title = title.into();
136        self
137    }
138
139    /// Set the border color when focused.
140    pub fn border_focused(mut self, color: Color) -> Self {
141        self.border_focused = color;
142        self
143    }
144
145    /// Set the border color when not focused.
146    pub fn border_unfocused(mut self, color: Color) -> Self {
147        self.border_unfocused = color;
148        self
149    }
150
151    /// Set the size constraints.
152    pub fn size(
153        mut self,
154        width_percent: u16,
155        height_percent: u16,
156        max_width: u16,
157        max_height: u16,
158    ) -> Self {
159        self.width_percent = width_percent;
160        self.height_percent = height_percent;
161        self.max_width = max_width;
162        self.max_height = max_height;
163        self
164    }
165
166    /// Set minimum size constraints.
167    pub fn min_size(mut self, min_width: u16, min_height: u16) -> Self {
168        self.min_width = min_width;
169        self.min_height = min_height;
170        self
171    }
172
173    /// Set the search placeholder text.
174    pub fn search_placeholder(mut self, text: impl Into<String>) -> Self {
175        self.search_placeholder = text.into();
176        self
177    }
178
179    /// Get the style for a focused border.
180    pub fn focused_border_style(&self) -> Style {
181        Style::default().fg(self.border_focused)
182    }
183
184    /// Get the style for an unfocused border.
185    pub fn unfocused_border_style(&self) -> Style {
186        Style::default().fg(self.border_unfocused)
187    }
188
189    /// Get the style for the title.
190    pub fn title_style(&self) -> Style {
191        Style::default()
192            .fg(self.title_color)
193            .add_modifier(Modifier::BOLD)
194    }
195
196    /// Get the style for selected items.
197    pub fn selected_style(&self) -> Style {
198        Style::default()
199            .fg(self.selected_fg)
200            .bg(self.selected_bg)
201            .add_modifier(Modifier::BOLD)
202    }
203
204    /// Get the style for selected items without bold.
205    pub fn selected_text_style(&self) -> Style {
206        Style::default().fg(self.selected_fg).bg(self.selected_bg)
207    }
208
209    /// Get the style for global hotkeys.
210    pub fn global_key_style(&self) -> Style {
211        Style::default()
212            .fg(self.global_key_color)
213            .add_modifier(Modifier::BOLD)
214    }
215
216    /// Get the style for local hotkeys.
217    pub fn local_key_style(&self) -> Style {
218        Style::default()
219            .fg(self.local_key_color)
220            .add_modifier(Modifier::BOLD)
221    }
222
223    /// Get the style for locked indicators.
224    pub fn locked_style(&self) -> Style {
225        Style::default().fg(self.locked_color)
226    }
227
228    /// Get the style for normal text.
229    pub fn text_style(&self) -> Style {
230        Style::default().fg(self.text_color)
231    }
232
233    /// Get the style for dimmed/secondary text.
234    pub fn dim_style(&self) -> Style {
235        Style::default().fg(self.dim_color)
236    }
237
238    /// Get the style for placeholder text.
239    pub fn placeholder_style(&self) -> Style {
240        Style::default().fg(self.placeholder_color)
241    }
242
243    /// Get the style for the cursor.
244    pub fn cursor_style(&self) -> Style {
245        Style::default().fg(self.cursor_color)
246    }
247
248    /// Calculate the modal area dimensions.
249    pub fn calculate_modal_area(
250        &self,
251        screen_width: u16,
252        screen_height: u16,
253    ) -> (u16, u16, u16, u16) {
254        let modal_width = (screen_width * self.width_percent / 100)
255            .min(self.max_width)
256            .max(self.min_width)
257            .min(screen_width.saturating_sub(4));
258
259        let modal_height = (screen_height * self.height_percent / 100)
260            .min(self.max_height)
261            .max(self.min_height)
262            .min(screen_height.saturating_sub(4));
263
264        let x = (screen_width.saturating_sub(modal_width)) / 2;
265        let y = (screen_height.saturating_sub(modal_height)) / 2;
266
267        (x, y, modal_width, modal_height)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_default_style() {
277        let style = HotkeyDialogStyle::default();
278        assert_eq!(style.width_percent, 85);
279        assert_eq!(style.height_percent, 85);
280        assert_eq!(style.border_focused, Color::Yellow);
281    }
282
283    #[test]
284    fn test_builder_pattern() {
285        let style = HotkeyDialogStyle::new()
286            .title("My Hotkeys")
287            .border_focused(Color::Cyan)
288            .size(80, 80, 100, 40);
289
290        assert_eq!(style.title, "My Hotkeys");
291        assert_eq!(style.border_focused, Color::Cyan);
292        assert_eq!(style.width_percent, 80);
293    }
294
295    #[test]
296    fn test_calculate_modal_area() {
297        let style = HotkeyDialogStyle::default();
298        let (x, _y, w, _h) = style.calculate_modal_area(120, 40);
299
300        // 85% of 120 = 102, capped at max_width 110, so 102
301        assert!(w <= 110);
302        assert!(w >= 70);
303        // Should be centered
304        assert_eq!(x, (120 - w) / 2);
305    }
306}