retroglyph_widgets/widget/
button.rs1use retroglyph_core::{Backend, Color, Rect, Style, Terminal};
3
4use super::Widget;
5use crate::Response;
6use crate::Theme;
7use crate::draw::fill_rect;
8use crate::text::truncate as truncate_to_cols;
9
10#[derive(Clone, Copy, Debug)]
49pub struct Button<'a> {
50 label: &'a str,
51 response: Response,
52 style: Style,
53 hovered_style: Style,
54 pressed_style: Style,
55 focused_style: Style,
56}
57
58impl<'a> Button<'a> {
59 #[must_use]
61 pub fn new(label: &'a str, response: Response) -> Self {
62 Self {
63 label,
64 response,
65 style: Style::new()
66 .fg(Color::Rgb {
67 r: 170,
68 g: 175,
69 b: 190,
70 })
71 .bg(Color::Rgb {
72 r: 45,
73 g: 48,
74 b: 58,
75 }),
76 hovered_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
77 r: 60,
78 g: 65,
79 b: 80,
80 }),
81 pressed_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
82 r: 40,
83 g: 60,
84 b: 90,
85 }),
86 focused_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
87 r: 55,
88 g: 55,
89 b: 70,
90 }),
91 }
92 }
93
94 #[must_use]
96 pub const fn style(mut self, style: Style) -> Self {
97 self.style = style;
98 self
99 }
100
101 #[must_use]
103 pub const fn hovered_style(mut self, style: Style) -> Self {
104 self.hovered_style = style;
105 self
106 }
107
108 #[must_use]
110 pub const fn pressed_style(mut self, style: Style) -> Self {
111 self.pressed_style = style;
112 self
113 }
114
115 #[must_use]
118 pub const fn focused_style(mut self, style: Style) -> Self {
119 self.focused_style = style;
120 self
121 }
122
123 #[must_use]
130 pub fn theme(self, theme: Theme) -> Self {
131 self.theme_on(theme, theme.panel_bg)
132 }
133
134 #[must_use]
140 pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
141 self.style = Style::new().fg(theme.fg).bg(bg);
142 self.hovered_style = Style::new().fg(theme.fg).bg(theme.hover_bg);
143 self.pressed_style = Style::new().fg(theme.fg).bg(theme.press_bg);
144 self.focused_style = Style::new().fg(theme.accent).bg(bg);
145 self
146 }
147
148 const fn resolved_style(&self) -> Style {
151 if self.response.pressed() {
152 self.pressed_style
153 } else if self.response.hovered() {
154 self.hovered_style
155 } else if self.response.focused() {
156 self.focused_style
157 } else {
158 self.style
159 }
160 }
161}
162
163impl<B: Backend> Widget<B> for Button<'_> {
164 fn render(self, area: Rect, term: &mut Terminal<B>) {
165 if area.width() == 0 || area.height() == 0 {
166 return;
167 }
168
169 let style = self.resolved_style();
170 fill_rect(term, area, ' ', style);
171
172 let text = truncate_to_cols(self.label, area.width_usize());
173 let text_width = text.chars().count() as u16;
174 let x = area.left() + (area.width().saturating_sub(text_width)) / 2;
175 let y = area.top() + area.height() / 2;
176
177 term.reset_style()
178 .fg(style.foreground())
179 .bg(style.background());
180 term.print(x, y, text);
181 term.reset_style();
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use retroglyph_core::{
188 Event, Headless, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, Pos,
189 };
190
191 use super::*;
192 use crate::{Interaction, Sense};
193
194 #[derive(Clone, Copy, PartialEq, Eq)]
195 enum Id {
196 Save,
197 }
198
199 #[test]
200 fn draws_the_label_centered_in_the_idle_style() {
201 let area = Rect::new(0, 0, 7, 1);
202 let mut term = Terminal::new(Headless::new(7, 1));
203 Button::new("Go", Response::default()).render(area, &mut term);
204
205 assert_eq!(term.grid().get(2, 0).glyph(), 'G');
207 assert_eq!(term.grid().get(3, 0).glyph(), 'o');
208 }
209
210 #[test]
211 fn fills_the_whole_area_with_the_background() {
212 let area = Rect::new(0, 0, 7, 1);
213 let mut term = Terminal::new(Headless::new(7, 1));
214 Button::new("Go", Response::default()).render(area, &mut term);
215
216 let idle_bg = Style::new()
217 .fg(Color::Rgb {
218 r: 170,
219 g: 175,
220 b: 190,
221 })
222 .bg(Color::Rgb {
223 r: 45,
224 g: 48,
225 b: 58,
226 })
227 .background();
228 assert_eq!(term.grid().get(0, 0).style().background(), idle_bg);
229 assert_eq!(term.grid().get(6, 0).style().background(), idle_bg);
230 }
231
232 #[test]
233 fn pressed_takes_precedence_over_hovered() {
234 let response = Response {
235 hovered: true,
236 pressed: true,
237 ..Response::default()
238 };
239 let button = Button::new("Go", response);
240 assert_eq!(
241 button.resolved_style().background(),
242 button.pressed_style.background()
243 );
244 }
245
246 #[test]
247 fn hovered_takes_precedence_over_focused() {
248 let response = Response {
249 hovered: true,
250 focused: true,
251 ..Response::default()
252 };
253 let button = Button::new("Go", response);
254 assert_eq!(
255 button.resolved_style().background(),
256 button.hovered_style.background()
257 );
258 }
259
260 #[test]
261 fn focused_only_shows_when_not_pressed_or_hovered() {
262 let response = Response {
263 focused: true,
264 ..Response::default()
265 };
266 let button = Button::new("Go", response);
267 assert_eq!(
268 button.resolved_style().background(),
269 button.focused_style.background()
270 );
271 }
272
273 #[test]
274 fn idle_by_default() {
275 let button = Button::new("Go", Response::default());
276 assert_eq!(
277 button.resolved_style().background(),
278 button.style.background()
279 );
280 }
281
282 #[test]
283 fn style_knobs_can_be_overridden() {
284 let custom = Style::new().fg(Color::RED).bg(Color::GREEN);
285 let response = Response {
286 pressed: true,
287 ..Response::default()
288 };
289 let button = Button::new("Go", response).pressed_style(custom);
290 assert_eq!(button.resolved_style().background(), Color::GREEN);
291 }
292
293 #[test]
294 fn integrates_with_interaction_and_reflects_a_real_click() {
295 let mut interaction = Interaction::<Id>::new();
296 let area = Rect::new(0, 0, 7, 1);
297
298 interaction.begin_frame();
299 let _ = interaction.interact(area, Id::Save, Sense::click());
300 interaction.end_frame();
301
302 interaction.handle_event(&Event::Mouse(MouseEvent {
303 kind: MouseEventKind::Down(MouseButton::Left),
304 position: Pos::new(2, 0),
305 pixel_position: None,
306 modifiers: KeyModifiers::NONE,
307 }));
308 interaction.handle_event(&Event::Mouse(MouseEvent {
309 kind: MouseEventKind::Up(MouseButton::Left),
310 position: Pos::new(2, 0),
311 pixel_position: None,
312 modifiers: KeyModifiers::NONE,
313 }));
314
315 interaction.begin_frame();
316 let response = interaction.interact(area, Id::Save, Sense::click());
317 interaction.end_frame();
318 assert!(response.clicked());
319
320 let button = Button::new("Go", response);
326 assert_eq!(
327 button.resolved_style().background(),
328 button.pressed_style.background()
329 );
330
331 let mut term = Terminal::new(Headless::new(7, 1));
332 button.render(area, &mut term);
333 }
334
335 #[test]
336 fn zero_size_is_a_no_op() {
337 let area = Rect::new(0, 0, 0, 1);
338 let mut term = Terminal::new(Headless::new(1, 1));
339 Button::new("Go", Response::default()).render(area, &mut term);
340 assert_eq!(term.grid().get(0, 0).glyph(), ' ');
341 }
342
343 #[test]
344 fn theme_maps_named_roles_onto_every_state() {
345 use crate::Theme;
346
347 let response = Response {
348 hovered: true,
349 ..Response::default()
350 };
351 let button = Button::new("Go", response).theme(Theme::DARK);
352
353 assert_eq!(button.style.foreground(), Theme::DARK.fg);
354 assert_eq!(button.style.background(), Theme::DARK.panel_bg);
355 assert_eq!(button.hovered_style.background(), Theme::DARK.hover_bg);
356 assert_eq!(button.pressed_style.background(), Theme::DARK.press_bg);
357 assert_eq!(button.focused_style.foreground(), Theme::DARK.accent);
358 assert_eq!(button.resolved_style().background(), Theme::DARK.hover_bg);
359 }
360
361 #[test]
362 fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
363 use crate::Theme;
364
365 let button = Button::new("Go", Response::default()).theme_on(Theme::DARK, Color::Default);
366
367 assert_eq!(button.style.foreground(), Theme::DARK.fg);
368 assert_eq!(button.style.background(), Color::Default);
369 assert_eq!(button.focused_style.foreground(), Theme::DARK.accent);
370 assert_eq!(button.focused_style.background(), Color::Default);
371 assert_eq!(button.hovered_style.background(), Theme::DARK.hover_bg);
373 assert_eq!(button.pressed_style.background(), Theme::DARK.press_bg);
374 }
375}