fresh/view/controls/button/
mod.rs1mod input;
12mod render;
13
14use ratatui::layout::Rect;
15use ratatui::style::Color;
16
17pub use input::ButtonEvent;
18pub use render::{render_button, render_button_row};
19
20use super::FocusState;
21
22#[derive(Debug, Clone)]
24pub struct ButtonState {
25 pub label: String,
27 pub focus: FocusState,
29 pub pressed: bool,
31}
32
33impl ButtonState {
34 pub fn new(label: impl Into<String>) -> Self {
36 Self {
37 label: label.into(),
38 focus: FocusState::Normal,
39 pressed: false,
40 }
41 }
42
43 pub fn with_focus(mut self, focus: FocusState) -> Self {
45 self.focus = focus;
46 self
47 }
48
49 pub fn is_enabled(&self) -> bool {
51 self.focus != FocusState::Disabled
52 }
53
54 pub fn set_pressed(&mut self, pressed: bool) {
56 self.pressed = pressed;
57 }
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct ButtonColors {
63 pub text: Color,
65 pub border: Color,
67 pub pressed_bg: Color,
69 pub focused: Color,
71 pub hovered: Color,
73 pub disabled: Color,
75}
76
77impl Default for ButtonColors {
78 fn default() -> Self {
79 Self {
80 text: Color::White,
81 border: Color::Gray,
82 pressed_bg: Color::DarkGray,
83 focused: Color::Cyan,
84 hovered: Color::Blue,
85 disabled: Color::DarkGray,
86 }
87 }
88}
89
90impl ButtonColors {
91 pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
93 Self {
94 text: theme.editor_fg,
95 border: theme.line_number_fg,
96 pressed_bg: theme.selection_bg,
97 focused: theme.selection_bg,
98 hovered: theme.menu_hover_bg,
99 disabled: theme.line_number_fg,
100 }
101 }
102
103 pub fn primary() -> Self {
105 Self {
106 text: Color::Black,
107 border: Color::Cyan,
108 pressed_bg: Color::LightCyan,
109 focused: Color::Cyan,
110 hovered: Color::LightCyan,
111 disabled: Color::DarkGray,
112 }
113 }
114
115 pub fn danger() -> Self {
117 Self {
118 text: Color::White,
119 border: Color::Red,
120 pressed_bg: Color::LightRed,
121 focused: Color::Red,
122 hovered: Color::LightRed,
123 disabled: Color::DarkGray,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, Default)]
130pub struct ButtonLayout {
131 pub button_area: Rect,
133}
134
135impl ButtonLayout {
136 pub fn contains(&self, x: u16, y: u16) -> bool {
138 x >= self.button_area.x
139 && x < self.button_area.x + self.button_area.width
140 && y >= self.button_area.y
141 && y < self.button_area.y + self.button_area.height
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use ratatui::backend::TestBackend;
149 use ratatui::Terminal;
150
151 fn test_frame<F>(width: u16, height: u16, f: F)
152 where
153 F: FnOnce(&mut ratatui::Frame, Rect),
154 {
155 let backend = TestBackend::new(width, height);
156 let mut terminal = Terminal::new(backend).unwrap();
157 terminal
158 .draw(|frame| {
159 let area = Rect::new(0, 0, width, height);
160 f(frame, area);
161 })
162 .unwrap();
163 }
164
165 #[test]
166 fn test_button_renders() {
167 test_frame(20, 1, |frame, area| {
168 let state = ButtonState::new("OK");
169 let colors = ButtonColors::default();
170 let layout = render_button(frame, area, &state, &colors);
171
172 assert_eq!(layout.button_area.width, 6); });
174 }
175
176 #[test]
177 fn test_button_hit_detection() {
178 test_frame(20, 1, |frame, area| {
179 let state = ButtonState::new("Click");
180 let colors = ButtonColors::default();
181 let layout = render_button(frame, area, &state, &colors);
182
183 assert!(layout.contains(0, 0));
185 assert!(layout.contains(5, 0));
186
187 assert!(!layout.contains(15, 0));
189 });
190 }
191
192 #[test]
193 fn test_button_row() {
194 test_frame(40, 1, |frame, area| {
195 let ok = ButtonState::new("OK");
196 let cancel = ButtonState::new("Cancel");
197 let colors = ButtonColors::default();
198
199 let layouts = render_button_row(frame, area, &[(&ok, &colors), (&cancel, &colors)], 2);
200
201 assert_eq!(layouts.len(), 2);
202 assert!(layouts[0].button_area.x < layouts[1].button_area.x);
203 });
204 }
205
206 #[test]
207 fn test_button_disabled() {
208 let state = ButtonState::new("Save").with_focus(FocusState::Disabled);
209 assert!(!state.is_enabled());
210 }
211
212 #[test]
213 fn test_button_pressed_state() {
214 let mut state = ButtonState::new("Submit");
215 assert!(!state.pressed);
216
217 state.set_pressed(true);
218 assert!(state.pressed);
219 }
220
221 #[test]
222 fn test_button_truncation() {
223 test_frame(8, 1, |frame, area| {
224 let state = ButtonState::new("Very Long Button Text");
225 let colors = ButtonColors::default();
226 let layout = render_button(frame, area, &state, &colors);
227
228 assert!(layout.button_area.width <= area.width);
230 });
231 }
232}