retroglyph_widgets/widget/
button.rs1use retroglyph_core::{Color, Rect, Style};
3
4use super::Widget;
5use crate::Response;
6use crate::Surface;
7use crate::Theme;
8use crate::draw::fill_rect;
9use crate::text::truncate as truncate_to_cols;
10
11#[derive(Clone, Copy, Debug)]
50pub struct Button<'a> {
51 label: &'a str,
52 response: Response,
53 style: Style,
54 hovered_style: Style,
55 pressed_style: Style,
56 focused_style: Style,
57}
58
59impl<'a> Button<'a> {
60 #[must_use]
62 pub fn new(label: &'a str, response: Response) -> Self {
63 Self {
64 label,
65 response,
66 style: Style::new()
67 .fg(Color::Rgb {
68 r: 170,
69 g: 175,
70 b: 190,
71 })
72 .bg(Color::Rgb {
73 r: 45,
74 g: 48,
75 b: 58,
76 }),
77 hovered_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
78 r: 60,
79 g: 65,
80 b: 80,
81 }),
82 pressed_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
83 r: 40,
84 g: 60,
85 b: 90,
86 }),
87 focused_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
88 r: 55,
89 g: 55,
90 b: 70,
91 }),
92 }
93 }
94
95 #[must_use]
97 pub const fn style(mut self, style: Style) -> Self {
98 self.style = style;
99 self
100 }
101
102 #[must_use]
104 pub const fn hovered_style(mut self, style: Style) -> Self {
105 self.hovered_style = style;
106 self
107 }
108
109 #[must_use]
111 pub const fn pressed_style(mut self, style: Style) -> Self {
112 self.pressed_style = style;
113 self
114 }
115
116 #[must_use]
119 pub const fn focused_style(mut self, style: Style) -> Self {
120 self.focused_style = style;
121 self
122 }
123
124 #[must_use]
131 pub fn theme(self, theme: Theme) -> Self {
132 self.theme_on(theme, theme.panel_bg)
133 }
134
135 #[must_use]
141 pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
142 self.style = Style::new().fg(theme.fg).bg(bg);
143 self.hovered_style = Style::new().fg(theme.fg).bg(theme.hover_bg);
144 self.pressed_style = Style::new().fg(theme.fg).bg(theme.press_bg);
145 self.focused_style = Style::new().fg(theme.accent).bg(bg);
146 self
147 }
148
149 const fn resolved_style(&self) -> Style {
152 if self.response.pressed() {
153 self.pressed_style
154 } else if self.response.hovered() {
155 self.hovered_style
156 } else if self.response.focused() {
157 self.focused_style
158 } else {
159 self.style
160 }
161 }
162}
163
164impl Widget for Button<'_> {
165 fn render(&self, area: Rect, surface: &mut Surface<'_>) {
166 if area.width() == 0 || area.height() == 0 {
167 return;
168 }
169
170 let style = self.resolved_style();
171 fill_rect(surface, area, ' ', style);
172
173 let text = truncate_to_cols(self.label, area.width_usize());
174 #[allow(clippy::cast_possible_truncation)]
177 let text_width = text.chars().count() as u16;
178 let x = area.left() + (area.width().saturating_sub(text_width)) / 2;
179 let y = area.top() + area.height() / 2;
180
181 surface.print((x, y), text, style);
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use retroglyph_core::{
188 Event, Grid, 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 grid = Grid::new(7, 1);
203 Button::new("Go", Response::default()).render(area, &mut Surface::new(&mut grid, area, 0));
204
205 assert_eq!(grid[Pos::new(2, 0)].glyph(), 'G');
207 assert_eq!(grid[Pos::new(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 grid = Grid::new(7, 1);
214 Button::new("Go", Response::default()).render(area, &mut Surface::new(&mut grid, area, 0));
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!(grid[Pos::new(0, 0)].style().background(), idle_bg);
229 assert_eq!(grid[Pos::new(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 grid = Grid::new(7, 1);
332 button.render(area, &mut Surface::new(&mut grid, area, 0));
333 }
334
335 #[test]
336 fn zero_size_is_a_no_op() {
337 let area = Rect::new(0, 0, 0, 1);
338 let mut grid = Grid::new(1, 1);
339 Button::new("Go", Response::default()).render(area, &mut Surface::new(&mut grid, area, 0));
340 assert_eq!(grid[Pos::new(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}